Class: Menuconform::Menu
- Inherits:
-
Object
- Object
- Menuconform::Menu
- Defined in:
- lib/menuconform/menu.rb
Overview
Read-only wrapper around a schema-valid Menu IR document. Provides the indexes and graph traversals the rules share. Tolerates semantically broken menus (dangling refs, duplicate ids, cycles) by design — reporting those is the rules' job, so every accessor here must stay total.
Instance Attribute Summary collapse
-
#doc ⇒ Object
readonly
Returns the value of attribute doc.
-
#reference_time ⇒ Object
readonly
Returns the value of attribute reference_time.
Instance Method Summary collapse
-
#attached_group_ids(item) ⇒ Object
Groups attached directly to the item (depth 1), including slot groups.
- #categories ⇒ Object
- #categories_by_id ⇒ Object
-
#depth_info(item) ⇒ Object
{ max_depth: Integer, cyclic: Boolean } — depth counted item->group = 1.
-
#duplicate_ids ⇒ Object
{ "item" => [dup ids], "modifier_group" => [...], ... }.
-
#each_conditional_context ⇒ Object
Yields every (group, option-with-conditional-prices, trigger context) pair in item context, deduped per (group, option).
- #groups_by_id ⇒ Object
-
#groups_containing_modifier ⇒ Object
modifier id -> [group ids whose options include it].
-
#initialize(doc, reference_time: Time.now.utc) ⇒ Menu
constructor
A new instance of Menu.
- #items ⇒ Object
-
#items_by_id ⇒ Object
First definition wins on duplicate ids; duplicates are reported by STRUCT-003.
- #modifier_groups ⇒ Object
- #modifiers ⇒ Object
- #modifiers_by_id ⇒ Object
-
#reachable_group_ids(item) ⇒ Object
Every existing group reachable from the item through option-level nesting.
-
#referenced_group_ids ⇒ Object
Group ids referenced anywhere (items, slots, option children).
-
#referenced_modifier_ids ⇒ Object
Modifier ids referenced anywhere (options, conditional-price triggers).
-
#resolved_min_price(option) ⇒ Object
Cheapest possible price for one unit of this option, over base price, link override, and all conditional prices.
Constructor Details
#initialize(doc, reference_time: Time.now.utc) ⇒ Menu
Returns a new instance of Menu.
13 14 15 16 |
# File 'lib/menuconform/menu.rb', line 13 def initialize(doc, reference_time: Time.now.utc) @doc = doc @reference_time = reference_time end |
Instance Attribute Details
#doc ⇒ Object (readonly)
Returns the value of attribute doc.
11 12 13 |
# File 'lib/menuconform/menu.rb', line 11 def doc @doc end |
#reference_time ⇒ Object (readonly)
Returns the value of attribute reference_time.
11 12 13 |
# File 'lib/menuconform/menu.rb', line 11 def reference_time @reference_time end |
Instance Method Details
#attached_group_ids(item) ⇒ Object
Groups attached directly to the item (depth 1), including slot groups.
40 41 42 43 |
# File 'lib/menuconform/menu.rb', line 40 def attached_group_ids(item) (item["modifier_group_ids"] || []) + (item["slots"] || []).flat_map { |s| s["modifier_group_ids"] || [] } end |
#categories ⇒ Object
18 |
# File 'lib/menuconform/menu.rb', line 18 def categories = @doc["categories"] || [] |
#categories_by_id ⇒ Object
27 |
# File 'lib/menuconform/menu.rb', line 27 def categories_by_id = @categories_by_id ||= index_first(categories) |
#depth_info(item) ⇒ Object
{ max_depth: Integer, cyclic: Boolean } — depth counted item->group = 1.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/menuconform/menu.rb', line 63 def depth_info(item) cyclic = false depth_of = lambda do |gid, path| g = groups_by_id[gid] return 0 if g.nil? if path.include?(gid) cyclic = true return 0 end child_ids = (g["options"] || []).flat_map { |o| o["child_modifier_group_ids"] || [] } 1 + child_ids.map { |cid| depth_of.call(cid, path + [gid]) }.max.to_i end max = attached_group_ids(item).map { |gid| depth_of.call(gid, []) }.max.to_i { max_depth: max, cyclic: cyclic } end |
#duplicate_ids ⇒ Object
{ "item" => [dup ids], "modifier_group" => [...], ... }
30 31 32 33 34 35 36 37 |
# File 'lib/menuconform/menu.rb', line 30 def duplicate_ids @duplicate_ids ||= { "category" => dups(categories), "item" => dups(items), "modifier_group" => dups(modifier_groups), "modifier" => dups(modifiers) } end |
#each_conditional_context ⇒ Object
Yields every (group, option-with-conditional-prices, trigger context) pair in item context, deduped per (group, option). Trigger context is one of:
{ status: :fallback } no trigger group reachable — legal
{ status: :valid, group: <group> } single reachable max_select:1 group
{ status: :invalid } multi-select or spanning trigger
Options with a dangling when_modifier_id are skipped (STRUCT-002 owns those).
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/menuconform/menu.rb', line 122 def each_conditional_context seen = Set.new items_by_id.each_value do |item| reach = reachable_group_ids(item) reach.each do |gid| g = groups_by_id[gid] (g["options"] || []).each do |o| cps = o["conditional_prices"] || [] next if cps.empty? key = [gid, o["modifier_id"]] next if seen.include?(key) when_ids = cps.map { |cp| cp["when_modifier_id"] } next if when_ids.any? { |w| !modifiers_by_id.key?(w) } trigger_gids = when_ids.flat_map { |w| groups_containing_modifier[w] } .select { |tg| reach.include?(tg) }.uniq next if trigger_gids.empty? # fallback pricing; nothing to check seen << key if trigger_gids.size == 1 && groups_by_id[trigger_gids.first]["max_select"] == 1 yield g, o, { status: :valid, group: groups_by_id[trigger_gids.first] } else yield g, o, { status: :invalid } end end end end end |
#groups_by_id ⇒ Object
25 |
# File 'lib/menuconform/menu.rb', line 25 def groups_by_id = @groups_by_id ||= index_first(modifier_groups) |
#groups_containing_modifier ⇒ Object
modifier id -> [group ids whose options include it]
106 107 108 109 110 111 112 113 114 |
# File 'lib/menuconform/menu.rb', line 106 def groups_containing_modifier @groups_containing_modifier ||= begin map = Hash.new { |h, k| h[k] = [] } groups_by_id.each_value do |g| (g["options"] || []).each { |o| map[o["modifier_id"]] << g["id"] } end map end end |
#items ⇒ Object
19 |
# File 'lib/menuconform/menu.rb', line 19 def items = @doc["items"] || [] |
#items_by_id ⇒ Object
First definition wins on duplicate ids; duplicates are reported by STRUCT-003.
24 |
# File 'lib/menuconform/menu.rb', line 24 def items_by_id = @items_by_id ||= index_first(items) |
#modifier_groups ⇒ Object
20 |
# File 'lib/menuconform/menu.rb', line 20 def modifier_groups = @doc["modifier_groups"] || [] |
#modifiers ⇒ Object
21 |
# File 'lib/menuconform/menu.rb', line 21 def modifiers = @doc["modifiers"] || [] |
#modifiers_by_id ⇒ Object
26 |
# File 'lib/menuconform/menu.rb', line 26 def modifiers_by_id = @modifiers_by_id ||= index_first(modifiers) |
#reachable_group_ids(item) ⇒ Object
Every existing group reachable from the item through option-level nesting.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/menuconform/menu.rb', line 46 def reachable_group_ids(item) seen = Set.new frontier = attached_group_ids(item) until frontier.empty? nxt = [] frontier.each do |gid| g = groups_by_id[gid] next if g.nil? || seen.include?(gid) seen << gid (g["options"] || []).each { |o| nxt.concat(o["child_modifier_group_ids"] || []) } end frontier = nxt end seen end |
#referenced_group_ids ⇒ Object
Group ids referenced anywhere (items, slots, option children).
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/menuconform/menu.rb', line 80 def referenced_group_ids @referenced_group_ids ||= begin refs = Set.new items.each { |it| attached_group_ids(it).each { |gid| refs << gid } } modifier_groups.each do |g| (g["options"] || []).each { |o| (o["child_modifier_group_ids"] || []).each { |gid| refs << gid } } end refs end end |
#referenced_modifier_ids ⇒ Object
Modifier ids referenced anywhere (options, conditional-price triggers).
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/menuconform/menu.rb', line 92 def referenced_modifier_ids @referenced_modifier_ids ||= begin refs = Set.new modifier_groups.each do |g| (g["options"] || []).each do |o| refs << o["modifier_id"] (o["conditional_prices"] || []).each { |cp| refs << cp["when_modifier_id"] } end end refs end end |
#resolved_min_price(option) ⇒ Object
Cheapest possible price for one unit of this option, over base price, link override, and all conditional prices.
151 152 153 154 155 156 |
# File 'lib/menuconform/menu.rb', line 151 def resolved_min_price(option) mod = modifiers_by_id[option["modifier_id"]] return 0 if mod.nil? base = option.key?("price_override") ? option["price_override"] : mod["price"] ([base] + (option["conditional_prices"] || []).map { |cp| cp["price"] }).min end |