hecks 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/check_era.rb +3 -39
  10. data/lib/hecks/cli/interview_draft.rb +86 -31
  11. data/lib/hecks/cli/interview_session.rb +77 -26
  12. data/lib/hecks/cli/interview_shape.rb +179 -0
  13. data/lib/hecks/doors/cli_runner.rb +14 -1
  14. data/lib/hecks/doors/launcher_options.rb +13 -3
  15. data/lib/hecks/doors/mcp_door.rb +16 -2
  16. data/lib/hecks/doors/mcp_door_scope.rb +45 -5
  17. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  18. data/lib/hecks/doors.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  20. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  21. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  22. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  23. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  24. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  25. data/lib/hecks/gate/stages.yml +113 -8
  26. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  28. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  29. data/lib/hecks/hecks/adapters/git.rb +59 -0
  30. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  31. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  32. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  33. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  34. data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
  35. data/lib/hecks/hecks/codebase.behaviors +6 -0
  36. data/lib/hecks/hecks/codebase.bluebook +21 -0
  37. data/lib/hecks/hecks/custodian.behaviors +6 -0
  38. data/lib/hecks/hecks/custodian.bluebook +217 -0
  39. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  40. data/lib/hecks/hecks/hecks.world +8 -3
  41. data/lib/hecks/hecks/site.world +5 -2
  42. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  43. data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
  44. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  45. data/lib/hecks/language/oidc.json +5 -0
  46. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  47. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  48. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  49. data/lib/hecks/projections/deploy/box/settings.rb +40 -6
  50. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  51. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  52. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  53. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  54. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  55. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  56. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  57. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  58. data/lib/hecks/projections/deploy/box.rb +28 -4
  59. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  60. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  61. data/lib/hecks/projections/model/deviations.rb +1 -1
  62. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  63. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  64. data/lib/hecks/projections/site/edge.rb +25 -11
  65. data/lib/hecks/projections/site/live_edge.rb +162 -0
  66. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  67. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  68. data/lib/hecks/projections/site/table/checks.rb +53 -7
  69. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  70. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  71. data/lib/hecks/projections/site/table.rb +16 -5
  72. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  73. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  74. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  75. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  76. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  77. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  78. data/lib/hecks/runtime/interpreting.rb +3 -22
  79. data/lib/hecks/runtime/needs.rb +48 -0
  80. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  81. data/lib/hecks/runtime/registry/verification.rb +3 -3
  82. data/lib/hecks/rust_build/host.rb +147 -0
  83. data/lib/hecks/rust_build/kernel_input.rb +51 -17
  84. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  85. data/lib/hecks/rust_build/project_rust.rb +7 -140
  86. data/lib/hecks/rust_build.rb +1 -0
  87. data/lib/hecks/site/bluebook/site.bluebook +80 -14
  88. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  89. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  90. data/lib/hecks/storehouse.rb +35 -1
  91. data/lib/hecks/tools/comment_style.rb +4 -2
  92. data/lib/hecks/tools/gate.rb +1 -1
  93. data/lib/hecks/tools/rust_comment_style.rb +2 -4
  94. data/lib/hecks/tools/site_routes.rb +69 -28
  95. data/lib/hecks/tools/tools_doc.rb +204 -0
  96. data/lib/hecks/tools.rb +1 -0
  97. data/lib/hecks/version.rb +1 -1
  98. data/lib/hecks/vocabulary.rb +152 -1
  99. data/lib/hecks.rb +1 -0
  100. data/qa/settings.yml +1 -2
  101. data/rust/build/src/lineage_pass.rs +31 -16
  102. data/rust/build/src/main.rs +4 -2
  103. data/rust/build/src/pipeline.rs +9 -20
  104. data/rust/codegen/src/attr.rs +1 -1
  105. data/rust/codegen/src/bridging.rs +8 -8
  106. data/rust/codegen/src/commands.rs +1 -1
  107. data/rust/codegen/src/constraints.rs +1 -1
  108. data/rust/codegen/src/dependency_planning.rs +1 -1
  109. data/rust/codegen/src/domain_generator.rs +2 -2
  110. data/rust/codegen/src/exemplar.rs +1 -1
  111. data/rust/codegen/src/expr_emitter.rs +1 -1
  112. data/rust/codegen/src/fielded.rs +1 -1
  113. data/rust/codegen/src/json.rs +214 -23
  114. data/rust/codegen/src/json_codec.rs +1 -1
  115. data/rust/codegen/src/literal.rs +6 -3
  116. data/rust/codegen/src/main.rs +23 -13
  117. data/rust/codegen/src/mutations.rs +1 -1
  118. data/rust/codegen/src/naming.rs +5 -3
  119. data/rust/codegen/src/optional_pass.rs +226 -0
  120. data/rust/codegen/src/ports.rs +1 -1
  121. data/rust/codegen/src/queries.rs +3 -3
  122. data/rust/codegen/src/reactions.rs +1 -1
  123. data/rust/codegen/src/read_models.rs +2 -2
  124. data/rust/codegen/src/reference_specs.rs +1 -1
  125. data/rust/codegen/src/registry.rs +4 -4
  126. data/rust/codegen/src/shared.rs +1 -1
  127. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  128. data/rust/codegen/src/skip_reason.rs +1 -1
  129. data/rust/codegen/src/types.rs +4 -4
  130. data/rust/host/HECKS_RELEASE +1 -1
  131. data/rust/host/src/api.rs +1 -1
  132. data/rust/host/src/dispatch.rs +5 -0
  133. data/rust/host/src/needs.rs +193 -29
  134. data/rust/host/src/resend.rs +4 -4
  135. data/rust/host/src/ui_schema.rs +1 -1
  136. data/rust/parser/src/emit.rs +12 -0
  137. data/rust/parser/src/ir.rs +2 -0
  138. data/rust/parser/src/keywords.rs +2 -0
  139. data/rust/parser/src/main.rs +1 -0
  140. data/rust/parser/src/parse/command.rs +2 -44
  141. data/rust/parser/src/parse/mod.rs +1 -0
  142. data/rust/parser/src/parse/needs.rs +60 -0
  143. data/rust/parser/src/parse/query.rs +3 -0
  144. data/rust/src/exemplar/commands.rs +1 -1
  145. data/rust/src/exemplar/constraints.rs +1 -1
  146. data/rust/src/exemplar/fielded.rs +1 -1
  147. data/rust/src/exemplar/json.rs +1 -1
  148. data/rust/src/exemplar/mod.rs +1 -1
  149. data/rust/src/exemplar/mutations.rs +1 -1
  150. data/rust/src/exemplar/queries.rs +1 -1
  151. data/rust/src/exemplar/reactions.rs +1 -1
  152. data/rust/src/exemplar/read_models.rs +1 -1
  153. data/rust/src/exemplar/registry.rs +1 -1
  154. data/rust/src/exemplar/types.rs +1 -1
  155. data/rust/src/kernel/cli.rs +18 -8
  156. data/rust/src/kernel/needs.rs +104 -9
  157. data/rust/src/kernel/read_model.rs +1 -1
  158. data/rust/src/lib.rs +1 -1
  159. metadata +24 -30
  160. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  161. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  162. data/lib/hecks/three_zero/forms.yml +0 -127
  163. data/lib/hecks/three_zero.rb +0 -12
  164. data/rust/build/src/json.rs +0 -421
  165. data/rust/build/src/optional_pass.rs +0 -150
  166. data/rust/project/bridging.rb +0 -257
  167. data/rust/project/commands.rb +0 -877
  168. data/rust/project/constraints.rb +0 -86
  169. data/rust/project/dependency_planning.rb +0 -178
  170. data/rust/project/domain_generator.rb +0 -983
  171. data/rust/project/exemplar.rb +0 -148
  172. data/rust/project/expr_emitter.rb +0 -72
  173. data/rust/project/fielded.rb +0 -141
  174. data/rust/project/json_codec.rb +0 -641
  175. data/rust/project/mutations.rb +0 -695
  176. data/rust/project/naming.rb +0 -223
  177. data/rust/project/ports.rb +0 -65
  178. data/rust/project/queries.rb +0 -493
  179. data/rust/project/reactions.rb +0 -317
  180. data/rust/project/read_models.rb +0 -690
  181. data/rust/project/reference_specs.rb +0 -27
  182. data/rust/project/registry.rb +0 -396
  183. data/rust/project/skip_reason.rb +0 -29
  184. data/rust/project/types.rb +0 -225
  185. data/rust/project/write_if_changed.rb +0 -7
  186. data/rust/project.rb +0 -30
  187. data/rust/project_rust_pipeline.rb +0 -391
@@ -1,150 +0,0 @@
1
- //! Marks value-object and entity fields `optional` when an `append` mutation binds them to an
2
- //! optional command argument, so the generated struct field is an `Option`.
3
-
4
- use crate::json::Json;
5
-
6
- pub fn run(ir: &mut Json) {
7
- if let Some(aggregates) = ir.get_mut("aggregates").and_then(Json::as_array_mut) {
8
- for aggregate in aggregates.iter_mut() {
9
- mark_append_optional_fields(aggregate);
10
- }
11
- }
12
- }
13
-
14
- enum ElementRef {
15
- ValueObject(usize),
16
- Entity(usize),
17
- }
18
-
19
- fn find_element_ref(aggregate: &Json, target_type: &str) -> Option<ElementRef> {
20
- if let Some(idx) = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]).iter().position(|vo| vo.get("name").and_then(Json::as_str) == Some(target_type)) {
21
- return Some(ElementRef::ValueObject(idx));
22
- }
23
- if let Some(idx) = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().position(|e| e.get("name").and_then(Json::as_str) == Some(target_type)) {
24
- return Some(ElementRef::Entity(idx));
25
- }
26
- None
27
- }
28
-
29
- // Gathers the fields to mark first, then applies them: the commands are read while the
30
- // value objects and entities are mutated.
31
- fn mark_append_optional_fields(aggregate: &mut Json) {
32
- let target_attrs: Vec<(String, String)> = aggregate
33
- .get("attributes")
34
- .map(Json::each)
35
- .unwrap_or(&[])
36
- .iter()
37
- .filter_map(|a| {
38
- let name = a.get("name").and_then(Json::as_str)?;
39
- let type_name = a.get("type").and_then(Json::as_str)?;
40
- Some((name.to_string(), type_name.to_string()))
41
- })
42
- .collect();
43
-
44
- for (attr_name, attr_type) in target_attrs {
45
- let Some(element_ref) = find_element_ref(aggregate, &attr_type) else { continue };
46
-
47
- let fields_to_mark = collect_fields_to_mark(aggregate, &attr_name);
48
- if fields_to_mark.is_empty() {
49
- continue;
50
- }
51
-
52
- apply_marks(aggregate, &element_ref, &fields_to_mark);
53
- }
54
- }
55
-
56
- fn collect_fields_to_mark(aggregate: &Json, target_attr_name: &str) -> Vec<String> {
57
- let mut out = Vec::new();
58
-
59
- for command in aggregate.get("commands").map(Json::each).unwrap_or(&[]) {
60
- let command_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
61
-
62
- for mutation in command.get("mutations").map(Json::each).unwrap_or(&[]) {
63
- let op = mutation.get("op").and_then(Json::as_str).unwrap_or("");
64
- let target = mutation.get("target").and_then(Json::as_str).unwrap_or("");
65
- if op != "append" || target != target_attr_name {
66
- continue;
67
- }
68
-
69
- let Some(fields) = mutation.get("fields").and_then(Json::as_object) else { continue };
70
- for (field_name, source) in fields {
71
- // Only a Symbol (an argument name) renders with a leading `:` in `Literal.render`.
72
- let Some(source_text) = source.as_str() else { continue };
73
- let Some(arg_name) = source_text.strip_prefix(':') else { continue };
74
-
75
- let source_is_optional = command_attrs.iter().any(|a| {
76
- a.get("name").and_then(Json::as_str) == Some(arg_name) && a.get("optional").map(Json::as_bool).unwrap_or(false)
77
- });
78
- if source_is_optional {
79
- out.push(field_name.clone());
80
- }
81
- }
82
- }
83
- }
84
-
85
- out
86
- }
87
-
88
- fn apply_marks(aggregate: &mut Json, element_ref: &ElementRef, fields_to_mark: &[String]) {
89
- let element = match element_ref {
90
- ElementRef::ValueObject(idx) => aggregate.get_mut("value_objects").and_then(Json::as_array_mut).and_then(|v| v.get_mut(*idx)),
91
- ElementRef::Entity(idx) => aggregate.get_mut("entities").and_then(Json::as_array_mut).and_then(|v| v.get_mut(*idx)),
92
- };
93
- let Some(element) = element else { return };
94
- let Some(attrs) = element.get_mut("attributes").and_then(Json::as_array_mut) else { return };
95
-
96
- for field_name in fields_to_mark {
97
- if let Some(field_attr) = attrs.iter_mut().find(|a| a.get("name").and_then(Json::as_str) == Some(field_name.as_str())) {
98
- field_attr.set_bool("optional", true);
99
- }
100
- }
101
- }
102
-
103
- #[cfg(test)]
104
- mod tests {
105
- use super::*;
106
-
107
- // Banking's `Account.Credit` shape: entity list `ledger`, one `append` binding `amount` from
108
- // an optional argument and `direction` from a literal Hash.
109
- fn fixture() -> Json {
110
- Json::parse(
111
- r#"{
112
- "attributes": [
113
- {"name": "ledger", "type": "LedgerEntry", "list": true, "default": null, "optional": false, "pattern": null, "admits": null}
114
- ],
115
- "value_objects": [],
116
- "entities": [
117
- {
118
- "name": "LedgerEntry",
119
- "attributes": [
120
- {"name": "amount", "type": "Money", "list": false, "default": null, "optional": false, "pattern": null, "admits": null},
121
- {"name": "direction", "type": "Direction", "list": false, "default": null, "optional": false, "pattern": null, "admits": null}
122
- ]
123
- }
124
- ],
125
- "commands": [
126
- {
127
- "name": "Credit",
128
- "attributes": [
129
- {"name": "amount", "type": "PositiveMoney", "list": false, "default": null, "optional": true, "pattern": null, "admits": null}
130
- ],
131
- "mutations": [
132
- {"target": "ledger", "op": "append", "fields": {"amount": ":amount", "direction": "{value: \"credit\"}"}}
133
- ]
134
- }
135
- ]
136
- }"#,
137
- )
138
- .expect("fixture parses")
139
- }
140
-
141
- #[test]
142
- fn marks_the_argument_sourced_field_optional() {
143
- let mut aggregate = fixture();
144
- mark_append_optional_fields(&mut aggregate);
145
-
146
- let entity = &aggregate.get("entities").unwrap().each()[0];
147
- let amount = entity.get("attributes").unwrap().each().iter().find(|a| a.get("name").and_then(Json::as_str) == Some("amount")).unwrap();
148
- assert!(amount.get("optional").unwrap().as_bool());
149
- }
150
- }
@@ -1,257 +0,0 @@
1
- module RustProjection
2
- module Projector
3
- module_function
4
-
5
- # `use` statements for value-object types this aggregate's commands
6
- # reference by name but don't declare locally (same chapter only).
7
- def cross_aggregate_vo_imports(aggregate, domain_value_object_owner, mod_name)
8
- local_names = aggregate[:value_objects].map { |vo| vo[:name] }.to_set
9
- attrs = (aggregate[:commands] + aggregate[:entities].flat_map { |e| e[:commands] } +
10
- aggregate[:ports].flat_map { |p| p[:operations] })
11
- .flat_map { |c| c[:attributes] }
12
-
13
- # A locally-declared type is never "foreign", even if some other
14
- # aggregate owns a VO of the same name domain-wide.
15
- foreign_types = attrs.map { |a| a[:type] }.uniq.reject { |type| local_names.include?(type) }
16
- foreign_types.filter_map { |type| domain_value_object_owner[type] && [type, domain_value_object_owner[type]] }
17
- .sort_by { |(type, owner)| [owner, type] }
18
- .map { |(type, owner)| "use crate::generated::#{mod_name}::#{owner.downcase}::#{rust_ident(type)};" }
19
- end
20
-
21
- # Whether target_vo's fields can all be filled from source_vo's fields
22
- # (matched by name) or the target's own defaults.
23
- def vo_field_bridgeable?(source_vo, target_vo)
24
- return false unless source_vo && target_vo
25
- return closed_sets_bridgeable?(source_vo, target_vo) if source_vo[:closed_set] || target_vo[:closed_set]
26
-
27
- target_vo[:attributes].all? do |t_attr|
28
- source_vo[:attributes].any? { |s_attr| s_attr[:name] == t_attr[:name] } || !t_attr[:default].nil?
29
- end
30
- end
31
-
32
- # Source closed-set members mappable to target members, or nil unless
33
- # every source member is also a target member (so the bridge can't fail).
34
- def closed_set_bridge_members(source_vo, target_vo)
35
- return nil unless source_vo[:closed_set] && target_vo[:closed_set]
36
- return nil unless source_vo[:attributes].size == 1 && target_vo[:attributes].size == 1
37
-
38
- target_values = target_vo[:members].map { |row| row.to_h.values.first.to_s }
39
- source_rows = source_vo[:members]
40
- return nil unless source_rows.all? { |row| target_values.include?(row.to_h.values.first.to_s) }
41
-
42
- source_rows
43
- end
44
-
45
- def closed_sets_bridgeable?(source_vo, target_vo)
46
- !closed_set_bridge_members(source_vo, target_vo).nil?
47
- end
48
-
49
- def vo_field_rhs(source_expr, source_vo, target_type, value_objects_by_name)
50
- target_vo = value_objects_by_name[target_type]
51
- if (rows = closed_set_bridge_members(source_vo, target_vo))
52
- arms = rows.map { |row| "#{rust_ident(source_vo[:name])}::#{closed_set_variant(row)} => #{rust_ident(target_type)}::#{closed_set_variant(row)}" }
53
- return "match &#{source_expr} { #{arms.join(', ')} }"
54
- end
55
-
56
- fields = target_vo[:attributes].map do |t_attr|
57
- field = rust_ident_field(t_attr[:name])
58
- if source_vo[:attributes].any? { |s_attr| s_attr[:name] == t_attr[:name] }
59
- "#{field}: #{source_expr}.#{field}.clone()"
60
- else
61
- "#{field}: #{literal_rhs(t_attr[:default])}"
62
- end
63
- end
64
- "#{rust_ident(target_type)} { #{fields.join(', ')} }"
65
- end
66
-
67
- # Whether a value can move from source_type to target_type. Shared
68
- # with `value_rhs`, which performs the bridge this checks for.
69
- def bridgeable_value_types?(source_type, target_type, value_objects_by_name)
70
- return true if source_type == target_type
71
- # Compares Rust representations, not declared type names: a
72
- # `Reference<T>` and a bare `String` are both just `String`.
73
- return true if effective_scalar_type(source_type) && effective_scalar_type(source_type) == effective_scalar_type(target_type)
74
-
75
- source_vo = value_objects_by_name[source_type]
76
- target_vo = value_objects_by_name[target_type]
77
- return vo_field_bridgeable?(source_vo, target_vo) if target_vo
78
-
79
- # A single-field source VO unwraps into a bare scalar target when
80
- # that one field's own Rust representation already matches it.
81
- return false unless source_vo && !source_vo[:closed_set] && source_vo[:attributes].size == 1
82
-
83
- unwrapped_type = source_vo[:attributes].first[:type]
84
- unwrapped_type == target_type ||
85
- (effective_scalar_type(unwrapped_type) && effective_scalar_type(unwrapped_type) == effective_scalar_type(target_type))
86
- end
87
-
88
- def value_rhs(source_expr, source_type, target_type, value_objects_by_name)
89
- return "#{source_expr}.clone()" if source_type == target_type
90
- return "#{source_expr}.clone()" if effective_scalar_type(source_type) && effective_scalar_type(source_type) == effective_scalar_type(target_type)
91
-
92
- source_vo = value_objects_by_name[source_type]
93
- target_vo = value_objects_by_name[target_type]
94
- return vo_field_rhs(source_expr, source_vo, target_type, value_objects_by_name) if target_vo
95
-
96
- raise "unsupported coercion #{source_type} -> #{target_type} — bridgeable_value_types? should have caught this" unless source_vo && !source_vo[:closed_set] && source_vo[:attributes].size == 1
97
-
98
- "#{source_expr}.#{rust_ident_field(source_vo[:attributes].first[:name])}.clone()"
99
- end
100
-
101
- # Whether a list `:set` needs per-element rebuilding, rather than one
102
- # `.clone()`, because source and target elements differ in representation.
103
- def list_bridge_requires_element_mapping?(source_type, target_type)
104
- return false if source_type == target_type
105
-
106
- !(effective_scalar_type(source_type) && effective_scalar_type(source_type) == effective_scalar_type(target_type))
107
- end
108
-
109
- # `value_rhs`, mapped over each element of a list `:set` whose source
110
- # and target element types differ.
111
- def list_value_rhs(source_expr, source_type, target_type, value_objects_by_name)
112
- "#{source_expr}.iter().map(|item| #{value_rhs('item', source_type, target_type, value_objects_by_name)}).collect()"
113
- end
114
-
115
- def literal_set_bridgeable?(value, target_type, value_objects_by_name)
116
- return literal_hash_bridgeable?(value, target_type, value_objects_by_name) if value.is_a?(Hash) && target_type
117
- return false if value.is_a?(Hash)
118
- return false unless value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
119
- return true unless target_type && value_objects_by_name[target_type]
120
-
121
- # A scalar literal into a single-field VO stands for that field's
122
- # value (mirrors `Value.for_attribute`'s runtime rewrap).
123
- sole = sole_field_of(target_type, value_objects_by_name)
124
- return false unless sole
125
-
126
- literal_hash_bridgeable?({ sole => value }, target_type, value_objects_by_name)
127
- end
128
-
129
- # The right-hand side for a literal into target_type: a Hash, a
130
- # scalar rewrapped into a single-field VO, or the scalar itself.
131
- def literal_rhs_for(value, target_type, value_objects_by_name)
132
- return literal_hash_rhs(value, target_type, value_objects_by_name) if value.is_a?(Hash)
133
-
134
- sole = target_type && value_objects_by_name[target_type] && sole_field_of(target_type, value_objects_by_name)
135
- return literal_hash_rhs({ sole => value }, target_type, value_objects_by_name) if sole
136
-
137
- literal_rhs(value)
138
- end
139
-
140
- # Whether a literal Hash supplies every field a target VO (or one
141
- # of its closed-set members) needs.
142
- def literal_hash_bridgeable?(hash, target_type, value_objects_by_name)
143
- vo = value_objects_by_name[target_type]
144
- return false unless vo
145
-
146
- if vo[:closed_set]
147
- vo[:members].any? { |member| member.all? { |field, value| [hash[field.to_sym], hash[field.to_s]].include?(value) } }
148
- else
149
- vo[:attributes].all? { |attr| hash.key?(attr[:name].to_sym) || hash.key?(attr[:name].to_s) }
150
- end
151
- end
152
-
153
- def literal_hash_rhs(hash, target_type, value_objects_by_name)
154
- vo = value_objects_by_name[target_type]
155
- raise "unsupported literal hash source #{hash.inspect} -> #{target_type} — literal_hash_bridgeable? should have caught this" unless vo
156
-
157
- if vo[:closed_set]
158
- row = vo[:members].find { |member| member.all? { |field, value| [hash[field.to_sym], hash[field.to_s]].include?(value) } }
159
- raise "literal #{hash.inspect} matches no member of #{target_type} — literal_hash_bridgeable? should have caught this" unless row
160
-
161
- "#{rust_ident(target_type)}::#{closed_set_variant(row)}"
162
- else
163
- fields = vo[:attributes].map do |attr|
164
- key = [attr[:name].to_sym, attr[:name].to_s].find { |k| hash.key?(k) }
165
- raise "literal #{hash.inspect} missing field #{attr[:name]} for #{target_type} — literal_hash_bridgeable? should have caught this" unless key
166
-
167
- "#{rust_ident_field(attr[:name])}: #{literal_rhs(hash[key])}"
168
- end
169
- "#{rust_ident(target_type)} { #{fields.join(', ')} }"
170
- end
171
- end
172
-
173
- def integer_field_of(vo)
174
- return nil unless vo && !vo[:closed_set]
175
-
176
- attr = vo[:attributes].find { |a| a[:type] == "Integer" }
177
- attr && attr[:name]
178
- end
179
-
180
- # The attribute and its one Integer field an `:increment`/`:decrement`
181
- # touches, or nil unless the target is a single-field integer VO.
182
- def arithmetic_target_field(mutation, aggregate, value_objects_by_name)
183
- target_attr = aggregate[:attributes].find { |a| a[:name].to_s == mutation[:target].to_s }
184
- return nil unless target_attr && !target_attr[:list]
185
-
186
- field = integer_field_of(value_objects_by_name[target_attr[:type]])
187
- field && [target_attr, field]
188
- end
189
-
190
- # The amount side of an `:increment`/`:decrement` as a raw Rust
191
- # integer expression. Returns nil, not raise, when nothing bridges.
192
- def arithmetic_amount_expr(source, command, value_objects_by_name, target_integer_field)
193
- if source[:kind] == "literal"
194
- value = source[:value]
195
- return literal_rhs(value) if value.is_a?(Integer)
196
- return nil unless value.is_a?(Hash)
197
-
198
- key = [target_integer_field.to_sym, target_integer_field.to_s].find { |k| value.key?(k) }
199
- key && value[key].is_a?(Integer) ? literal_rhs(value[key]) : nil
200
- elsif source[:kind] == "argument"
201
- arg_attr = command[:attributes].find { |a| a[:name].to_s == source[:name] }
202
- return nil unless arg_attr
203
- return "args.#{rust_ident_field(arg_attr[:name])}" if arg_attr[:type] == "Integer"
204
-
205
- field = integer_field_of(value_objects_by_name[arg_attr[:type]])
206
- field && "args.#{rust_ident_field(arg_attr[:name])}.#{rust_ident_field(field)}"
207
- end
208
- end
209
-
210
- # `clamp:`'s literal `[min, max]` bounds, or nil for anything else
211
- # (non-literal source, wrong length, non-Integer bound).
212
- def clamp_bounds_ints(source)
213
- return nil unless source[:kind] == "literal"
214
-
215
- value = source[:value]
216
- return nil unless value.is_a?(Array) && value.size == 2 && value.all? { |v| v.is_a?(Integer) }
217
-
218
- value
219
- end
220
-
221
- # The default for an attribute a creating command's arguments don't
222
- # mention: its own default, or one built from its VO's field defaults.
223
- def creation_default_rhs(attr, value_objects_by_name)
224
- default = attr[:default]
225
- unless default.nil?
226
- return literal_rhs(default) unless default.is_a?(Hash)
227
-
228
- # Completes a partial Hash default with the target VO's own
229
- # per-field defaults first; `literal_hash_rhs` requires every
230
- # field present.
231
- return literal_hash_rhs(complete_hash_default(default, attr[:type], value_objects_by_name), attr[:type], value_objects_by_name)
232
- end
233
-
234
- vo = value_objects_by_name[attr[:type]]
235
- return nil unless vo && !vo[:closed_set] && vo[:attributes].all? { |f| !f[:default].nil? }
236
-
237
- fields = vo[:attributes].map { |f| "#{rust_ident_field(f[:name])}: #{literal_rhs(f[:default])}" }
238
- "#{rust_ident(attr[:type])} { #{fields.join(', ')} }"
239
- end
240
-
241
- # Fields present in `hash`, plus any the target VO defaults on its
242
- # own; a field with neither is left out, not set to nil.
243
- def complete_hash_default(hash, target_type, value_objects_by_name)
244
- vo = value_objects_by_name[target_type]
245
- return hash unless vo && !vo[:closed_set]
246
-
247
- vo[:attributes].each_with_object({}) do |field, completed|
248
- key = [field[:name].to_sym, field[:name].to_s].find { |k| hash.key?(k) }
249
- if key
250
- completed[field[:name].to_sym] = hash[key]
251
- elsif !field[:default].nil?
252
- completed[field[:name].to_sym] = field[:default]
253
- end
254
- end
255
- end
256
- end
257
- end