Class: Udb::ExtensionRequirementExpression

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Includes:
AbstractRequirement
Defined in:
lib/udb/req_expression.rb

Overview

represents a JSON Schema composition of extension requirements, e.g.:

anyOf:

- oneOf:
  - A
  - B
- C

Defined Under Namespace

Classes: LogicNode, TYPES

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(composition_hash, cfg_arch)

Parameters:

  • composition_hash (String, Hash{String => T.untyped})

    A possibly recursive hash of “allOf”, “anyOf”, “oneOf”, “not”, “if”

  • cfg_arch (ConfiguredArchitecture)


68
69
70
71
72
73
74
75
76
# File 'lib/udb/req_expression.rb', line 68

def initialize(composition_hash, cfg_arch)
  unless is_a_condition?(composition_hash)
    raise ArgumentError, "Expecting a JSON schema comdition (got #{composition_hash})"
  end

  @hsh = composition_hash
  @arch = cfg_arch
  @satisfied_by_cfg_arch = T.let({}, T::Hash[String, SatisfiedResult])
end

Class Method Details

.all_of(*conds, cfg_arch:)

This method returns an undefined value.

combine all conds into one using AND

Parameters:



264
265
266
267
268
269
270
# File 'lib/udb/req_expression.rb', line 264

def self.all_of(*conds, cfg_arch:)
  cond = ExtensionRequirementExpression.new({
    "allOf" => conds
  }, cfg_arch)

  ExtensionRequirementExpression.new(cond.minimize, cfg_arch)
end

Instance Method Details

#combos_for(extension_versions) ⇒ Array<Array<ExtensionVersion>>

Parameters:

Returns:



584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/udb/req_expression.rb', line 584

def combos_for(extension_versions)
  ncombos = extension_versions.reduce(1) { |prod, vers| prod * (vers.size + 1) }
  combos = T.let([], T::Array[T::Array[ExtensionVersion]])
  ncombos.times do |i|
    combos << []
    extension_versions.size.times do |j|
      m = (T.must(extension_versions[j]).size + 1)
      d = j.zero? ? 1 : T.must(extension_versions[j..0]).reduce(1) { |prod, vers| prod * (vers.size + 1) }

      if (i / d) % m < T.must(extension_versions[j]).size
        T.must(combos.last) << T.must(T.must(extension_versions[j])[(i / d) % m])
      end
    end
  end
  # get rid of any combos that can't happen because of extension conflicts
  combos.reject do |combo|
    combo.any? { |ext_ver1| (combo - [ext_ver1]).any? { |ext_ver2| ext_ver1.conflicts_condition.satisfied_by? { |ext_req| ext_req.satisfied_by?(ext_ver2) } } }
  end
end

#compatible?(other) ⇒ Boolean

Returns if it’s possible for both to be simultaneously true.

Parameters:

Returns:

  • (Boolean)

    if it’s possible for both to be simultaneously true



607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/udb/req_expression.rb', line 607

def compatible?(other)
  tree1 = to_logic_tree(@hsh)
  tree2 = to_logic_tree(other.to_h)

  extensions = (tree1.terms + tree2.terms).map(&:extension).uniq

  extension_versions = extensions.map(&:versions)

  combos = combos_for(extension_versions)
  combos.each do |combo|
    return true if tree1.eval(combo) && tree2.eval(combo)
  end

  # there is no combination in which both self and other can be true
  false
end

#could_be_satisfied_by_ext_reqs?(ext_req_list) ⇒ Boolean

returns true if the list of extension requirements can satisfy the condition.

note that it is possible that the condition might not be satisfied for all possible extension versions implied by ext_req_list. For example, this condition:

{ "name": "A", "version": ">= 1.2" }

Will be “satisfied by”:

{ "name": "A", "version": ">= 1.1" }

because A version 1.2 fits both requirements

Parameters:

Returns:

  • (Boolean)


691
692
693
694
695
696
697
698
699
# File 'lib/udb/req_expression.rb', line 691

def could_be_satisfied_by_ext_reqs?(ext_req_list)
  satisfied_by? do |cond_ext_req|
    ext_req_list.any? do |ext_req|
      ext_req.satisfying_versions.any? do |ext_ver|
        cond_ext_req.satisfied_by?(ext_ver)
      end
    end
  end
end

#could_be_true?(cfg_arch) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


649
650
651
652
# File 'lib/udb/req_expression.rb', line 649

def could_be_true?(cfg_arch)
  r = satisfied_by_cfg_arch?(cfg_arch)
  r == SatisfiedResult::Yes || r == SatisfiedResult::Maybe
end

#empty?Boolean

Returns:

  • (Boolean)


82
# File 'lib/udb/req_expression.rb', line 82

def empty? = false

#ext_req_to_logic_node(ext_req, term_idx, expand: true) ⇒ LogicNode

given an extension requirement, convert it to a LogicNode term, and optionally expand it to exclude any conflicts and include any implications

Parameters:

  • ext_req (ExtensionRequirement)

    An extension requirement

  • term_idx (Array<Integer>)
  • expand (Boolean) (defaults to: true)

    Whether or not to expand the node to include conflicts / implications

Returns:



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/udb/req_expression.rb', line 461

def ext_req_to_logic_node(ext_req, term_idx, expand: true)
  n = LogicNode.new(TYPES::Term, [ext_req], term_idx: term_idx[0])
  term_idx[0] = T.must(term_idx[0]) + 1
  if expand
    c = ext_req.extension.conflicts_condition
    unless c.empty?
      c = LogicNode.new(TYPES::Not, [to_logic_tree(ext_req.extension.data["conflicts"], term_idx:)])
      n = LogicNode.new(TYPES::And, [c, n])
    end

    ext_req.satisfying_versions.each do |ext_ver|
      ext_ver.implied_by_with_condition.each do |implied_by|
        implying_ext_ver = implied_by[:ext_ver]
        implying_cond = implied_by[:cond]
        implying_ext_req = ExtensionRequirement.new(implying_ext_ver.name, "= #{implying_ext_ver.version_str}", arch: @arch)
        if implying_cond.empty?
          # convert to an ext_req
          n = LogicNode.new(TYPES::Or, [n, ext_req_to_logic_node(implying_ext_req, term_idx)])
        else
          # conditional
          # convert to an ext_req
          cond_node = implying_cond.to_logic_tree(term_idx:, expand:)
          cond = LogicNode.new(TYPES::If, [cond_node, ext_req_to_logic_node(implying_ext_req, term_idx)])
          n = LogicNode.new(TYPES::Or, [n, cond])
        end
      end
    end
  end

  n
end

#first_requirement(req = @hsh) ⇒ ExtensionRequirement

Returns First requirement found, without considering any boolean operators.

Parameters:

  • req (String, Hash{String => T.untyped}, Array<T.untyped>) (defaults to: @hsh)

Returns:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/udb/req_expression.rb', line 241

def first_requirement(req = @hsh)
  case req
  when String
    ExtensionRequirement.new(req, [], arch: @arch)
  when Hash
    if req.key?("name")
      if req["version"].nil?
        ExtensionRequirement.new(req["name"], [], arch: @arch)
      else
        ExtensionRequirement.new(req["name"], req["version"], arch: @arch)
      end
    else
      first_requirement(req[T.must(req.keys[0])])
    end
  when Array
    first_requirement(req[0])
  else
    T.absurd(req)
  end
end

#flat?Boolean

Returns True if the condition is a join of N terms over the same operator

A or B or C #=> true A and B #=> true A or B and C #=> false.

Returns:

  • (Boolean)

    True if the condition is a join of N terms over the same operator

    A or B or C #=> true A and B #=> true A or B and C #=> false



103
104
105
106
107
108
109
110
# File 'lib/udb/req_expression.rb', line 103

def flat?
  case @hsh
  when String
    true
  when Hash
    @hsh.key?("name") || @hsh[T.must(@hsh.keys.first)].all? { |child| child.is_a?(String) || (child.is_a?(Hash) && child.key?("name")) }
  end
end

#flat_opSymbol

Returns The operator for a flat condition Only valid if #flat? is true.

Returns:

  • (Symbol)

    The operator for a flat condition Only valid if #flat? is true



115
116
117
118
119
120
121
122
# File 'lib/udb/req_expression.rb', line 115

def flat_op
  case @hsh
  when String
    TYPES::Or
  when Hash
    @hsh.key?("name") ? TYPES::Or : { "allOf" => TYPES::And, "anyOf" => TYPES::Or }[T.must(@hsh.keys.first)]
  end
end

#flat_versionsArray<ExtensionRequirement>

Returns The elements of the flat join Only valid if #flat? is true.

Returns:



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/udb/req_expression.rb', line 127

def flat_versions
  case @hsh
  when String
    [ExtensionRequirement.new(@hsh, [], arch: @arch)]
  when Hash
    if @hsh.key?("name")
      if @hsh.key?("version").nil?
        [ExtensionRequirement.new(@hsh["name"], [], arch: @arch)]
      else
        [ExtensionRequirement.new(@hsh["name"], @hsh["version"], arch: @arch)]
      end
    else
      @hsh[T.must(@hsh.keys.first)].map do |r|
        if r.is_a?(String)
          ExtensionRequirement.new(r, [], arch: @arch)
        else
          if r.key?("version").nil?
            ExtensionRequirement.new(r["name"], [], arch: @arch)
          else
            ExtensionRequirement.new(r["name"], r["version"], arch: @arch)
          end
        end
      end
    end
  end
end

#is_complex_condition_header?(cond) ⇒ Boolean

Parameters:

  • cond (String, Hash{String => T.untyped}, Array<T.untyped>)

Returns:

  • (Boolean)


168
169
170
171
# File 'lib/udb/req_expression.rb', line 168

def is_complex_condition_header?(cond)
  # Check if this is a complex condition header (allOf, anyOf, oneOf, etc.)
  cond.is_a?(Hash) && !cond.key?("name")
end

#is_simple_single_extension?(cond) ⇒ Boolean

Parameters:

  • cond (String, Hash{String => T.untyped}, Array<T.untyped>)

Returns:

  • (Boolean)


155
156
157
158
159
160
161
162
163
164
165
# File 'lib/udb/req_expression.rb', line 155

def is_simple_single_extension?(cond)
  case cond
  when String
    true
  when Hash
    # Single extension with name and optional version
    cond.key?("name") && cond.size <= 2 && (cond.size == 1 || cond.key?("version"))
  else
    false
  end
end

#minimize(hsh = @hsh) ⇒ String, Hash{String => T.untyped}

Returns Schema for this expression, with basic logic minimization.

Parameters:

  • hsh (String, Hash{String => T.untyped}) (defaults to: @hsh)

Returns:

  • (String, Hash{String => T.untyped})

    Schema for this expression, with basic logic minimization



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/udb/req_expression.rb', line 274

def minimize(hsh = @hsh)
  case hsh
  when Hash
    if hsh.key?("name")
      hsh
    else
      min_ary = key = nil
      if hsh.key?("allOf")
        min_ary = hsh["allOf"].map { |element| minimize(element) }
        key = "allOf"
      elsif hsh.key?("anyOf")
        min_ary = hsh["anyOf"].map { |element| minimize(element) }
        key = "anyOf"
      elsif hsh.key?("oneOf")
        min_ary = hsh["oneOf"].map { |element| minimize(element) }
        key = "oneOf"
      elsif hsh.key?("not")
        min_ary = hsh.dup
        key = "not"
      elsif hsh.key?("if")
        return hsh
      end
      min_ary = min_ary.uniq
      if min_ary.size == 1
        min_ary.first
      else
        { key => min_ary }
      end
    end
  else
    hsh
  end
end

#possibly_satisfied_by?(ext_ver) ⇒ Boolean

yes if:

- ext_ver affects this condition
- it is is possible for this condition to be true is ext_ver is implemented

Parameters:

Returns:

  • (Boolean)


705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
# File 'lib/udb/req_expression.rb', line 705

def possibly_satisfied_by?(ext_ver)
  logic_tree = to_logic_tree

  return false unless logic_tree.terms.any? { |ext_req| ext_req.satisfying_versions.include?(ext_ver) }

  # ok, so ext_ver affects this condition
  # is it possible to be true with ext_ver implemented?
  extensions = logic_tree.terms.map(&:extension).uniq

  extension_versions = extensions.map(&:versions)

  combos = combos_for(extension_versions)
  combos.any? do |combo|
    # replace ext_ver, since it doesn't change
    logic_tree.eval(combo.map { |ev| ev.name == ext_ver.name ? ext_ver : ev })
  end
end

#satisfied_by?(&block) {|obj| ... } ⇒ Boolean

Returns Whether or not the entire condition is satisfied.

Examples:

See if a string satisfies

cond = { "anyOf" => ["A", "B", "C"] }
string = "A"
cond.satisfied_by? { |endpoint| endpoint == string } #=> true
string = "D"
cond.satisfied_by? { |endpoint| endpoint == string } #=> false

See if an array satisfies

cond = { "allOf" => ["A", "B", "C"] }
ary = ["A", "B", "C", "D"]
cond.satisfied_by? { |endpoint| ary.include?(endpoint) } #=> true
ary = ["A", "B"]
cond.satisfied_by? { |endpoint| ary.include?(endpoint) } #=> false

Parameters:

Yield Parameters:

  • obj (Object)

    An endpoint in the condition

Yield Returns:

  • (Boolean)

    Whether or not obj is what you are looking for

Returns:

  • (Boolean)

    Whether or not the entire condition is satisfied

Raises:

  • (ArgumentError)


642
643
644
645
646
# File 'lib/udb/req_expression.rb', line 642

def satisfied_by?(&block)
  raise ArgumentError, "Expecting one argument to block" unless block.arity == 1

  eval to_rb
end

#satisfied_by_cfg_arch?(cfg_arch) ⇒ SatisfiedResult

Parameters:

Returns:



655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
# File 'lib/udb/req_expression.rb', line 655

def satisfied_by_cfg_arch?(cfg_arch)
  @satisfied_by_cfg_arch[cfg_arch.name] ||=
    if cfg_arch.fully_configured?
      if satisfied_by? { |ext_req| cfg_arch.transitive_implemented_extension_versions.any? { |ev| ext_req.satisfied_by?(ev) } }
        SatisfiedResult::Yes
      else
        SatisfiedResult::No
      end
    elsif cfg_arch.partially_configured?
      if satisfied_by? { |cond_ext_req| cfg_arch.mandatory_extension_reqs.any? { |cfg_ext_req| cond_ext_req.satisfied_by?(cfg_ext_req) }  }
        SatisfiedResult::Yes
      elsif satisfied_by? { |cond_ext_req| cfg_arch.possible_extension_versions.any? { |cfg_ext_ver| cond_ext_req.satisfied_by?(cfg_ext_ver) }  }
        SatisfiedResult::Maybe
      else
        SatisfiedResult::No
      end
    else
      # unconfig. everything flies
      SatisfiedResult::Yes
    end
end

#to_asciidoc(cond = @hsh, indent = 0, join: "\n") ⇒ String

Parameters:

  • cond (String, Hash{String => T.untyped}, Array<T.untyped>) (defaults to: @hsh)
  • indent (Integer) (defaults to: 0)
  • join (String) (defaults to: "\n")

Returns:

  • (String)


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/udb/req_expression.rb', line 174

def to_asciidoc(cond = @hsh, indent = 0, join: "\n")
  # For simple single extension OR complex condition headers at root level (indent = 0), don't show bullets
  use_bullets = !(indent == 0 && (is_simple_single_extension?(cond) || is_complex_condition_header?(cond)))
  bullet_prefix = use_bullets ? "#{'*' * indent} " : ""

  case cond
  when String
    "#{bullet_prefix}#{cond}, version >= #{T.must(@arch.extension(cond)).min_version}"
  when Hash
    if cond.key?("name")
      if cond.key?("version")
        "#{bullet_prefix}#{cond['name']}, version #{cond['version']}#{join}"
      else
        "#{bullet_prefix}#{cond['name']}, version >= #{T.must(@arch.extension(cond['name'])).min_version}#{join}"
      end
    else
      "#{bullet_prefix}#{cond.keys[0]}:#{join}" + to_asciidoc(cond[T.must(cond.keys[0])], indent + 1)
    end
  when Array
    # Arrays represent multiple items, so they need bullets for clarity
    # Use indent=1 at root level to ensure bullets are shown
    array_indent = indent == 0 ? 1 : indent
    cond.map { |e| to_asciidoc(e, array_indent) }.join(join)
  else
    T.absurd(cond)
  end
end

#to_hString, Hash{String => T.untyped}

Returns:

  • (String, Hash{String => T.untyped})


79
# File 'lib/udb/req_expression.rb', line 79

def to_h = @hsh

#to_logic_tree(hsh = @hsh, term_idx: [0], expand: true) ⇒ LogicNode

convert the YAML representation of an Extension Requirement Expression into a tree of LogicNodes. Also expands any Extension Requirement to include its conflicts / implications

Parameters:

  • hsh (String, Hash{String => T.untyped}) (defaults to: @hsh)
  • term_idx (Array<Integer>) (defaults to: [0])
  • expand (Boolean) (defaults to: true)

Returns:



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/udb/req_expression.rb', line 497

def to_logic_tree(hsh = @hsh, term_idx: [0], expand: true)
  root = T.let(nil, T.nilable(LogicNode))

  if hsh.is_a?(Hash)
    if hsh.key?("name")
      if hsh.key?("version")
        if hsh["version"].is_a?(String)
          ext_req_to_logic_node(ExtensionRequirement.new(hsh["name"], hsh["version"], arch: @arch), term_idx, expand:)
        elsif hsh["version"].is_a?(Array)
          ext_req_to_logic_node(ExtensionRequirement.new(hsh["name"], hsh["version"].map { |v| "'#{v}'" }.join(', '), arch: @arch), term_idx, expand:)
        else
          raise "unexpected"
        end
      else
        ext_req_to_logic_node(ExtensionRequirement.new(hsh["name"], [], arch: @arch), term_idx, expand:)
      end
    else
      key = hsh.keys[0]

      case key
      when "allOf"
        raise "unexpected" unless hsh["allOf"].is_a?(Array) && hsh["allOf"].size > 1

        root = LogicNode.new(TYPES::And, [to_logic_tree(hsh["allOf"][0], term_idx:, expand:), to_logic_tree(hsh["allOf"][1], term_idx:, expand:)])
        (2...hsh["allOf"].size).each do |i|
          root = LogicNode.new(TYPES::And, [root, to_logic_tree(hsh["allOf"][i], term_idx:, expand:)])
        end
        root
      when "anyOf"
        raise "unexpected: #{hsh}" unless hsh["anyOf"].is_a?(Array) && hsh["anyOf"].size > 1

        root = LogicNode.new(TYPES::Or, [to_logic_tree(hsh["anyOf"][0], term_idx:, expand:), to_logic_tree(hsh["anyOf"][1], term_idx:, expand:)])
        (2...hsh["anyOf"].size).each do |i|
          root = LogicNode.new(TYPES::Or, [root, to_logic_tree(hsh["anyOf"][i], term_idx:, expand:)])
        end
        root
      when "if"
        raise "unexpected" unless hsh.keys.size == 2 && hsh.keys[1] == "then"

        cond = to_logic_tree(hsh["if"], term_idx:, expand:)
        body = to_logic_tree(hsh["then"], term_idx:, expand:)
        LogicNode.new(TYPES::If, [cond, body])
      when "oneOf"
        # expand oneOf into AND
        roots = T.let([], T::Array[LogicNode])

        if hsh["oneOf"].size < 2
          to_logic_tree(hsh["oneOf"][0], term_idx:, expand:)
        else
          hsh["oneOf"].size.times do |k|
            root =
              if k.zero?
                LogicNode.new(TYPES::And, [to_logic_tree(hsh["oneOf"][0], term_idx:, expand:), LogicNode.new(TYPES::Not, [to_logic_tree(hsh["oneOf"][1], term_idx:, expand:)])])
              elsif k == 1
                LogicNode.new(TYPES::And, [LogicNode.new(TYPES::Not, [to_logic_tree(hsh["oneOf"][0], term_idx:, expand:)]), to_logic_tree(hsh["oneOf"][1], term_idx:, expand:)])
              else
                LogicNode.new(TYPES::And, [LogicNode.new(TYPES::Not, [to_logic_tree(hsh["oneOf"][0], term_idx:, expand:)]), LogicNode.new(TYPES::Not, [to_logic_tree(hsh["oneOf"][1], term_idx:, expand:)])])
              end
            (2...hsh["oneOf"].size).each do |i|
              root =
                if k == i
                  LogicNode.new(TYPES::And, [root, to_logic_tree(hsh["oneOf"][i], term_idx:, expand:)])
                else
                  LogicNode.new(TYPES::And, [root, LogicNode.new(TYPES::Not, [to_logic_tree(hsh["oneOf"][i], term_idx:, expand:)])])
                end
             end
            roots << root
          end

          root = LogicNode.new(TYPES::Or, [T.must(roots[0]), T.must(roots[1])])
          (2...roots.size).each do |i|
            root = LogicNode.new(TYPES::Or, [root, T.must(roots[i])])
          end
          root
        end
      when "not"
        LogicNode.new(TYPES::Not, [to_logic_tree(hsh["not"], term_idx:, expand:)])
      else
        raise "Unexpected"
      end
    end
  else
    ext_req_to_logic_node(ExtensionRequirement.new(hsh, [], arch: @arch), term_idx, expand:)
  end
end

#to_rbString

Given the name of a ruby array ary_name containing the available objects to test, return a string that can be eval’d to determine if the objects in ary_name meet the Condition

Parameters:

  • ary_name (String)

    Name of a ruby string in the eval binding

Returns:

  • (String)

    If the condition is met



360
361
362
# File 'lib/udb/req_expression.rb', line 360

def to_rb
  to_rb_helper(@hsh)
end

#to_rb_helper(hsh) ⇒ String

Parameters:

  • hsh (String, Hash{String => T.untyped})

Returns:

  • (String)


309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/udb/req_expression.rb', line 309

def to_rb_helper(hsh)
  if hsh.is_a?(Hash)
    if hsh.key?("name")
      if hsh.key?("version")
        if hsh["version"].is_a?(String)
          "(yield ExtensionRequirement.new('#{hsh["name"]}', '#{hsh["version"]}', arch: @arch))"
        elsif hsh["version"].is_a?(Array)
          "(yield ExtensionRequirement.new('#{hsh["name"]}', #{hsh["version"].map { |v| "'#{v}'" }.join(', ')}, arch: @arch))"
        else
          raise "unexpected"
        end
      else
        "(yield ExtensionRequirement.new('#{hsh["name"]}', [], arch: @arch))"
      end
    else
      key = hsh.keys[0]

      case key
      when "allOf"
        rb_str = hsh["allOf"].map { |element| to_rb_helper(element) }.join(' && ')
        "(#{rb_str})"
      when "anyOf"
        rb_str = hsh["anyOf"].map { |element| to_rb_helper(element) }.join(' || ')
        "(#{rb_str})"
      when "oneOf"
        rb_str = hsh["oneOf"].map { |element| to_rb_helper(element) }.join(', ')
        "([#{rb_str}].count(true) == 1)"
      when "not"
        rb_str = to_rb_helper(hsh["not"])
        "(!#{rb_str})"
      when "if"
        cond_rb_str = to_rb_helper(hsh["if"])
        body_rb_str = to_rb_helper(hsh["body"])
        "(#{body_rb_str}) if (#{cond_rb_str})"
      else
        raise "Unexpected"
        # "(yield #{hsh})"
      end
    end
  else
    "(yield ExtensionRequirement.new('#{hsh}', [], arch: @arch))"
  end
end