Class: Menuconform::NestedOptionsExporter

Inherits:
Object
  • Object
show all
Defined in:
lib/menuconform/nested_options_exporter.rb

Overview

EXPERIMENTAL export target for UCP proposal #821 ("Nested Options", Universal-Commerce-Protocol/ucp#821): option values may carry a product_id referencing another product; nesting is capped at 5 products deep; pricing is recursive addition; no selection constraints (min/max/defaults) and no non-additive pricing — both are the proposal's stated non-goals.

Mapping:

  • Every IR modifier group becomes a product option; every option link becomes an option value referencing a modifier-product. Nested groups recurse. Items with any priced required group are now representable — the class that is EXPORT-001 under the shipped catalog model.
  • A modifier-product is shared across links only when the link-resolved price and child groups match; a link price_override otherwise forces a duplicated product (#821 has no per-reference override — measured and reported as a menu-level finding).
  • Conditional prices export at base/override price (estimate): exact expression would require duplicating the whole subtree per trigger selection, the same explosion the proposal exists to avoid.
  • Depth: IR group-depth d costs d+1 products (item + one per level), so the 5-product cap holds IR depth <= 4; deeper items are EXPORT-001.
  • Selection constraints, included quantities, and non-full_price slot pricing are EXPORT-002 (proposal non-goals). Allergens / min_age / dietary / schedules are unchanged by #821 and carry the same findings as the shipped-catalog exporter.

Constant Summary collapse

PROPOSAL =
"Universal-Commerce-Protocol/ucp#821 (nested options, experimental)"
PROPOSAL_WITH_RULES =
"Universal-Commerce-Protocol/ucp#821 + ucp#824 (nested options + selection rules, experimental)"
PRODUCT_DEPTH_CAP =
5
DEPTH_CAP_WITH_RULES =

ucp#824 composes with #821; depth 6 per the #821 thread agreement.

6

Instance Method Summary collapse

Constructor Details

#initialize(catalog:, selection_rules: false) ⇒ NestedOptionsExporter

selection_rules: true adds ucp#824's selection_rule (on product options) and quantity_rule (on option values) — the IR's two-axis constraints, per-link quantity bounds, and defaults all become expressible, so the constraints loss class disappears from the findings.



41
42
43
44
45
46
# File 'lib/menuconform/nested_options_exporter.rb', line 41

def initialize(catalog:, selection_rules: false)
  @catalog = catalog
  @selection_rules = selection_rules
  @depth_cap = selection_rules ? DEPTH_CAP_WITH_RULES : PRODUCT_DEPTH_CAP
  @proposal = selection_rules ? PROPOSAL_WITH_RULES : PROPOSAL
end

Instance Method Details

#call(doc, reference_time: Time.now.utc) ⇒ Object

Returns { artifact: Hash, findings: [Finding] } (severities stamped).



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/menuconform/nested_options_exporter.rb', line 49

def call(doc, reference_time: Time.now.utc)
  menu = Menu.new(doc, reference_time: reference_time)
  findings = []
  products = []
  @modifier_products = {}
  @mod_dup_counts = Hash.new(0)

  findings << finding("EXPORT-002", "menu", doc["name"] || "(unnamed)",
                      "menu-level service windows have no UCP catalog construct (unchanged by #821)") if present?(doc["availability"])
  findings << finding("EXPORT-002", "menu", doc["name"] || "(unnamed)",
                      "special_hours have no UCP catalog construct (unchanged by #821)") if present?(doc["special_hours"])
  menu.categories_by_id.each_value do |c|
    next unless present?(c["availability"])
    findings << finding("EXPORT-002", "category", c["id"],
                        "category availability windows have no UCP catalog construct (unchanged by #821)")
  end

  menu.items_by_id.each_value do |item|
    next if menu.depth_info(item)[:cyclic]
    product, item_findings = export_item(menu, item)
    findings.concat(item_findings)
    products << product if product
  end

  duplicated = @mod_dup_counts.select { |_, n| n > 1 }
  unless duplicated.empty?
    extra = duplicated.values.sum - duplicated.size
    findings << finding("EXPORT-002", "menu", doc["name"] || "(unnamed)",
                        "#{duplicated.size} modifier(s) carry context-dependent prices and had to be duplicated into " \
                        "#{extra} extra product(s) — #821 option values have no per-reference price override " \
                        "(affected: #{duplicated.keys.sort.join(', ')})")
  end

  artifact = {
    "ucp_spec_version" => UcpExporter::UCP_SPEC_PIN,
    "experimental_proposal" => @proposal,
    "generated_by" => "menuconform #{VERSION}",
    "catalog" => { "products" => products + @modifier_products.values.sort_by { |p| p["id"] } }
  }
  { artifact: artifact, findings: findings.each { |f| f.severity = @catalog.severity(f.rule) } }
end