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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/cli/interview_draft.rb +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +45 -5
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +40 -6
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +28 -4
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +51 -17
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +7 -140
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +80 -14
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +2 -4
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +152 -1
- data/lib/hecks.rb +1 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +31 -16
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +9 -20
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +8 -8
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +23 -13
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +5 -3
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +193 -29
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +18 -8
- data/rust/src/kernel/needs.rs +104 -9
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +24 -30
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- data/rust/project_rust_pipeline.rb +0 -391
data/rust/codegen/src/main.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! `hecks-codegen` —
|
|
1
|
+
//! `hecks-codegen` — the IR-to-Rust-source generator, the only one (ADR 0086).
|
|
2
2
|
//! Usage: `hecks-codegen <prelude|domain|full> ...`, one subcommand per function below.
|
|
3
3
|
// Some ported `naming.rs` helpers are not called yet.
|
|
4
4
|
#![allow(dead_code)]
|
|
@@ -19,6 +19,7 @@ mod literal;
|
|
|
19
19
|
mod manifest;
|
|
20
20
|
mod mutations;
|
|
21
21
|
mod naming;
|
|
22
|
+
mod optional_pass;
|
|
22
23
|
mod ports;
|
|
23
24
|
mod prelude;
|
|
24
25
|
mod queries;
|
|
@@ -28,6 +29,7 @@ mod reference_specs;
|
|
|
28
29
|
mod registry;
|
|
29
30
|
mod reserved_names;
|
|
30
31
|
mod shared;
|
|
32
|
+
mod sidecars;
|
|
31
33
|
mod skip_reason;
|
|
32
34
|
mod types;
|
|
33
35
|
|
|
@@ -85,22 +87,29 @@ fn run_prelude(args: &[String]) -> Result<(), String> {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/// `hecks-codegen domain <ir.json> <source_label> <mod_name> <out_dir>` — writes one chapter's
|
|
88
|
-
/// `<aggregate>.rs`, `registry.rs`, `mod.rs
|
|
89
|
-
///
|
|
90
|
-
/// `metadata.rs` and `ir.json` are left to `hecks project_rust`.
|
|
90
|
+
/// `<aggregate>.rs`, `registry.rs`, `mod.rs`, `manifest.json` and its `ir.json`/`metadata.rs`
|
|
91
|
+
/// sidecars into `out_dir`.
|
|
91
92
|
fn run_domain(args: &[String]) -> Result<(), String> {
|
|
92
93
|
let [ir_path, source_label, mod_name, out_dir] = args else {
|
|
93
94
|
return Err("usage: hecks-codegen domain <ir.json> <source_label> <mod_name> <out_dir>".to_string());
|
|
94
95
|
};
|
|
95
96
|
|
|
96
|
-
let
|
|
97
|
-
let ir = Json::parse(&text).map_err(|e| format!("parsing {ir_path}: {e}"))?;
|
|
97
|
+
let ir = read_ir(ir_path)?;
|
|
98
98
|
|
|
99
99
|
let ex = exemplar::Exemplar::load();
|
|
100
100
|
write_domain(&ex, &ir, source_label, mod_name, out_dir)?;
|
|
101
101
|
Ok(())
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/// Reads one chapter's IR and prepares it for generation: marks the fields an `append` binds to an
|
|
105
|
+
/// optional argument. What it generates from and what it writes to `ir.json` are this same IR.
|
|
106
|
+
fn read_ir(path: &str) -> Result<Json, String> {
|
|
107
|
+
let text = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
|
|
108
|
+
let mut ir = Json::parse(&text).map_err(|e| format!("parsing {path}: {e}"))?;
|
|
109
|
+
optional_pass::run(&mut ir);
|
|
110
|
+
Ok(ir)
|
|
111
|
+
}
|
|
112
|
+
|
|
104
113
|
/// Writes one chapter's files into `out_dir` and returns the `GeneratedDomain`, so `run_full`
|
|
105
114
|
/// can union the registry, query and read-model definitions across chapters.
|
|
106
115
|
fn write_domain(
|
|
@@ -110,7 +119,7 @@ fn write_domain(
|
|
|
110
119
|
mod_name: &str,
|
|
111
120
|
out_dir: &str,
|
|
112
121
|
) -> Result<domain_generator::GeneratedDomain, String> {
|
|
113
|
-
// Refuse before writing anything
|
|
122
|
+
// Refuse before writing anything, so a name collision is reported before any file exists.
|
|
114
123
|
let aggregate_names: Vec<&str> = ir
|
|
115
124
|
.get("aggregates")
|
|
116
125
|
.map(Json::each)
|
|
@@ -147,6 +156,10 @@ fn write_domain(
|
|
|
147
156
|
std::fs::write(&manifest_path, &generated.manifest_json).map_err(|e| format!("writing {manifest_path}: {e}"))?;
|
|
148
157
|
println!("wrote {manifest_path}");
|
|
149
158
|
|
|
159
|
+
let (ir_json_path, metadata_path) = sidecars::write(std::path::Path::new(out_dir), ir, source_label)?;
|
|
160
|
+
println!("wrote {}", ir_json_path.display());
|
|
161
|
+
println!("wrote {}", metadata_path.display());
|
|
162
|
+
|
|
150
163
|
Ok(generated)
|
|
151
164
|
}
|
|
152
165
|
|
|
@@ -181,8 +194,7 @@ fn run_full(args: &[String]) -> Result<(), String> {
|
|
|
181
194
|
|
|
182
195
|
let ex = exemplar::Exemplar::load();
|
|
183
196
|
|
|
184
|
-
let
|
|
185
|
-
let target_ir = Json::parse(&target_text).map_err(|e| format!("parsing {target_ir_path}: {e}"))?;
|
|
197
|
+
let target_ir = read_ir(target_ir_path)?;
|
|
186
198
|
let target_domain_name = target_ir.get("name").and_then(Json::as_str).unwrap_or("").to_string();
|
|
187
199
|
|
|
188
200
|
let target_out_dir = format!("{out_root}/{target_mod_name}");
|
|
@@ -209,8 +221,7 @@ fn run_full(args: &[String]) -> Result<(), String> {
|
|
|
209
221
|
let chapter_ir_path = &args[i + 2];
|
|
210
222
|
i += 3;
|
|
211
223
|
|
|
212
|
-
let
|
|
213
|
-
let chapter_ir = Json::parse(&text).map_err(|e| format!("parsing {chapter_ir_path}: {e}"))?;
|
|
224
|
+
let chapter_ir = read_ir(chapter_ir_path)?;
|
|
214
225
|
let chapter_domain_name = chapter_ir.get("name").and_then(Json::as_str).unwrap_or("").to_string();
|
|
215
226
|
|
|
216
227
|
let chapter_out_dir = format!("{out_root}/{chapter_mod_name}");
|
|
@@ -257,8 +268,7 @@ fn run_full(args: &[String]) -> Result<(), String> {
|
|
|
257
268
|
puts_blank(&mut merged_rs);
|
|
258
269
|
puts_str(&mut merged_rs, &reactions::emit_identity_head_table(&ex, &merged_aggregates));
|
|
259
270
|
puts_blank(&mut merged_rs);
|
|
260
|
-
// Section order
|
|
261
|
-
// this file byte for byte.
|
|
271
|
+
// Section order is part of the output: `hecks regenerate_corpus --check` diffs this file byte for byte.
|
|
262
272
|
puts_str(&mut merged_rs, &reactions::emit_entity_identity_head_table(&ex, &merged_aggregates));
|
|
263
273
|
puts_blank(&mut merged_rs);
|
|
264
274
|
puts_str(&mut merged_rs, &reactions::emit_command_attributes_table(&ex, &merged_aggregates));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `mutations.rb`, function for function.
|
|
2
2
|
//! `mark_append_optional_fields!` is not ported: `Json` has no mutation API and the corpus
|
|
3
3
|
//! already declares `optional: true` on every field that pass would touch.
|
|
4
4
|
|
data/rust/codegen/src/naming.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `naming.rb`, mirrored function for function.
|
|
2
2
|
|
|
3
3
|
use crate::json::Json;
|
|
4
4
|
|
|
@@ -285,8 +285,10 @@ pub fn literal_rhs(literal: &crate::json::Json) -> String {
|
|
|
285
285
|
match literal {
|
|
286
286
|
crate::json::Json::String(s) => format!("{}.to_string()", ruby_inspect_string(s)),
|
|
287
287
|
// Separate arms so a whole-number Float renders as `0.0`, not an integer-typed `0`.
|
|
288
|
-
crate::json::Json::
|
|
289
|
-
|
|
288
|
+
crate::json::Json::Number(_) => match literal.number() {
|
|
289
|
+
Some(crate::json::Number::Int(n)) => n.to_string(),
|
|
290
|
+
_ => literal.to_s(),
|
|
291
|
+
},
|
|
290
292
|
crate::json::Json::Bool(b) => b.to_string(),
|
|
291
293
|
other => panic!("unsupported literal mutation source {other:?} — not one of String/Integer/Float/Boolean"),
|
|
292
294
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
//! Resolves the appended element as `Hecks::RustBuild::AppendOptionals` does in Ruby: a local
|
|
5
|
+
//! entity wins over any value object of the same name, a local value object over a domain-wide one,
|
|
6
|
+
//! and an `append` into another aggregate's value object marks that value object.
|
|
7
|
+
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
|
|
10
|
+
use crate::json::Json;
|
|
11
|
+
|
|
12
|
+
/// Where an appended element lives: its aggregate's index, then its own index in that aggregate.
|
|
13
|
+
#[derive(Clone, Copy)]
|
|
14
|
+
enum Element {
|
|
15
|
+
ValueObject(usize, usize),
|
|
16
|
+
Entity(usize, usize),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
struct Mark {
|
|
20
|
+
element: Element,
|
|
21
|
+
field: String,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn run(ir: &mut Json) {
|
|
25
|
+
let marks = collect_marks(ir);
|
|
26
|
+
apply_marks(ir, &marks);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn list<'a>(node: &'a Json, key: &str) -> &'a [Json] {
|
|
30
|
+
node.get(key).map(Json::each).unwrap_or(&[])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn name_of(node: &Json) -> Option<&str> {
|
|
34
|
+
node.get("name").and_then(Json::as_str)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Every value object in the domain by name; the last one declared wins, as a Ruby `to_h` does.
|
|
38
|
+
fn domain_wide_value_objects(aggregates: &[Json]) -> HashMap<&str, (usize, usize)> {
|
|
39
|
+
let mut by_name = HashMap::new();
|
|
40
|
+
for (a, aggregate) in aggregates.iter().enumerate() {
|
|
41
|
+
for (v, value_object) in list(aggregate, "value_objects").iter().enumerate() {
|
|
42
|
+
if let Some(name) = name_of(value_object) {
|
|
43
|
+
by_name.insert(name, (a, v));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
by_name
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn find_element(aggregates: &[Json], a: usize, domain_wide: &HashMap<&str, (usize, usize)>, type_name: &str) -> Option<Element> {
|
|
51
|
+
let aggregate = &aggregates[a];
|
|
52
|
+
if let Some(e) = list(aggregate, "entities").iter().position(|entity| name_of(entity) == Some(type_name)) {
|
|
53
|
+
return Some(Element::Entity(a, e));
|
|
54
|
+
}
|
|
55
|
+
if let Some(v) = list(aggregate, "value_objects").iter().rposition(|value_object| name_of(value_object) == Some(type_name)) {
|
|
56
|
+
return Some(Element::ValueObject(a, v));
|
|
57
|
+
}
|
|
58
|
+
domain_wide.get(type_name).map(|&(owner, v)| Element::ValueObject(owner, v))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read first, write after: the commands are read while the elements they mark are mutated.
|
|
62
|
+
fn collect_marks(ir: &Json) -> Vec<Mark> {
|
|
63
|
+
let aggregates = list(ir, "aggregates");
|
|
64
|
+
let domain_wide = domain_wide_value_objects(aggregates);
|
|
65
|
+
let mut marks = Vec::new();
|
|
66
|
+
|
|
67
|
+
for (a, aggregate) in aggregates.iter().enumerate() {
|
|
68
|
+
for attribute in list(aggregate, "attributes") {
|
|
69
|
+
let (Some(attr_name), Some(type_name)) = (name_of(attribute), attribute.get("type").and_then(Json::as_str)) else { continue };
|
|
70
|
+
let Some(element) = find_element(aggregates, a, &domain_wide, type_name) else { continue };
|
|
71
|
+
|
|
72
|
+
for field in collect_fields_to_mark(aggregate, attr_name) {
|
|
73
|
+
marks.push(Mark { element, field });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
marks
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn collect_fields_to_mark(aggregate: &Json, target_attr_name: &str) -> Vec<String> {
|
|
81
|
+
let mut out = Vec::new();
|
|
82
|
+
|
|
83
|
+
for command in list(aggregate, "commands") {
|
|
84
|
+
let command_attrs = list(command, "attributes");
|
|
85
|
+
|
|
86
|
+
for mutation in list(command, "mutations") {
|
|
87
|
+
let op = mutation.get("op").and_then(Json::as_str).unwrap_or("");
|
|
88
|
+
let target = mutation.get("target").and_then(Json::as_str).unwrap_or("");
|
|
89
|
+
if op != "append" || target != target_attr_name {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let Some(fields) = mutation.get("fields").and_then(Json::as_object) else { continue };
|
|
94
|
+
for (field_name, source) in fields {
|
|
95
|
+
// Only a Symbol (an argument name) renders with a leading `:` in `Literal.render`.
|
|
96
|
+
let Some(source_text) = source.as_str() else { continue };
|
|
97
|
+
let Some(arg_name) = source_text.strip_prefix(':') else { continue };
|
|
98
|
+
|
|
99
|
+
let source_is_optional = command_attrs
|
|
100
|
+
.iter()
|
|
101
|
+
.any(|a| name_of(a) == Some(arg_name) && a.get("optional").map(Json::as_bool).unwrap_or(false));
|
|
102
|
+
if source_is_optional {
|
|
103
|
+
out.push(field_name.clone());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
out
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn apply_marks(ir: &mut Json, marks: &[Mark]) {
|
|
113
|
+
let Some(aggregates) = ir.get_mut("aggregates").and_then(Json::as_array_mut) else { return };
|
|
114
|
+
|
|
115
|
+
for mark in marks {
|
|
116
|
+
let (a, key, index) = match mark.element {
|
|
117
|
+
Element::ValueObject(a, v) => (a, "value_objects", v),
|
|
118
|
+
Element::Entity(a, e) => (a, "entities", e),
|
|
119
|
+
};
|
|
120
|
+
let element = aggregates.get_mut(a).and_then(|aggregate| aggregate.get_mut(key)).and_then(Json::as_array_mut).and_then(|elements| elements.get_mut(index));
|
|
121
|
+
let Some(attrs) = element.and_then(|element| element.get_mut("attributes")).and_then(Json::as_array_mut) else { continue };
|
|
122
|
+
|
|
123
|
+
if let Some(field_attr) = attrs.iter_mut().find(|a| name_of(a) == Some(mark.field.as_str())) {
|
|
124
|
+
field_attr.set_bool("optional", true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[cfg(test)]
|
|
130
|
+
mod tests {
|
|
131
|
+
use super::*;
|
|
132
|
+
|
|
133
|
+
fn aggregate_with(entities: &str, value_objects: &str, attribute_type: &str) -> String {
|
|
134
|
+
format!(
|
|
135
|
+
r#"{{
|
|
136
|
+
"attributes": [
|
|
137
|
+
{{"name": "ledger", "type": "{attribute_type}", "list": true, "default": null, "optional": false, "pattern": null, "admits": null}}
|
|
138
|
+
],
|
|
139
|
+
"value_objects": [{value_objects}],
|
|
140
|
+
"entities": [{entities}],
|
|
141
|
+
"commands": [
|
|
142
|
+
{{
|
|
143
|
+
"name": "Credit",
|
|
144
|
+
"attributes": [
|
|
145
|
+
{{"name": "amount", "type": "PositiveMoney", "list": false, "default": null, "optional": true, "pattern": null, "admits": null}}
|
|
146
|
+
],
|
|
147
|
+
"mutations": [
|
|
148
|
+
{{"target": "ledger", "op": "append", "fields": {{"amount": ":amount", "direction": "{{value: \"credit\"}}"}}}}
|
|
149
|
+
]
|
|
150
|
+
}}
|
|
151
|
+
]
|
|
152
|
+
}}"#
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn element(amount_optional: bool) -> String {
|
|
157
|
+
format!(
|
|
158
|
+
r#"{{"name": "LedgerEntry", "attributes": [
|
|
159
|
+
{{"name": "amount", "type": "Money", "list": false, "default": null, "optional": {amount_optional}, "pattern": null, "admits": null}},
|
|
160
|
+
{{"name": "direction", "type": "Direction", "list": false, "default": null, "optional": false, "pattern": null, "admits": null}}
|
|
161
|
+
]}}"#
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn domain(aggregates: &[String]) -> Json {
|
|
166
|
+
Json::parse(&format!(r#"{{"aggregates": [{}]}}"#, aggregates.join(","))).expect("fixture parses")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn amount_optional(ir: &Json, aggregate: usize, key: &str) -> bool {
|
|
170
|
+
let element = &list(&list(ir, "aggregates")[aggregate], key)[0];
|
|
171
|
+
let amount = list(element, "attributes").iter().find(|a| name_of(a) == Some("amount")).unwrap();
|
|
172
|
+
amount.get("optional").unwrap().as_bool()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The input and expectation `spec/rust_build_generation_helpers_spec.rb` holds the Ruby pass to,
|
|
176
|
+
// recorded from it. Read at test time, not compiled in: an installed gem has no `spec/`.
|
|
177
|
+
#[test]
|
|
178
|
+
fn marks_what_the_ruby_pass_marks_on_the_shared_fixture() {
|
|
179
|
+
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../spec/fixtures/append_optionals");
|
|
180
|
+
let read = |name: &str| Json::parse(&std::fs::read_to_string(dir.join(name)).expect("reads the fixture")).expect("parses");
|
|
181
|
+
|
|
182
|
+
let mut ir = read("input.json");
|
|
183
|
+
run(&mut ir);
|
|
184
|
+
|
|
185
|
+
assert_eq!(ir, read("expected.json"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Banking's `Account.Credit` shape: entity list `ledger`, one `append` binding `amount` from
|
|
189
|
+
// an optional argument and `direction` from a literal Hash.
|
|
190
|
+
#[test]
|
|
191
|
+
fn marks_the_argument_sourced_field_optional() {
|
|
192
|
+
let mut ir = domain(&[aggregate_with(&element(false), "", "LedgerEntry")]);
|
|
193
|
+
run(&mut ir);
|
|
194
|
+
|
|
195
|
+
assert!(amount_optional(&ir, 0, "entities"));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn marks_a_value_object_another_aggregate_declares() {
|
|
200
|
+
let owner = r#"{"attributes": [], "value_objects": [LEDGER], "entities": [], "commands": []}"#.replace("LEDGER", &element(false));
|
|
201
|
+
let appender = aggregate_with("", "", "LedgerEntry");
|
|
202
|
+
let mut ir = domain(&[appender, owner]);
|
|
203
|
+
run(&mut ir);
|
|
204
|
+
|
|
205
|
+
assert!(amount_optional(&ir, 1, "value_objects"), "the value object lives in the second aggregate");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[test]
|
|
209
|
+
fn prefers_a_local_entity_over_a_value_object_of_the_same_name() {
|
|
210
|
+
let local = aggregate_with(&element(false), "", "LedgerEntry");
|
|
211
|
+
let owner = r#"{"attributes": [], "value_objects": [LEDGER], "entities": [], "commands": []}"#.replace("LEDGER", &element(false));
|
|
212
|
+
let mut ir = domain(&[local, owner]);
|
|
213
|
+
run(&mut ir);
|
|
214
|
+
|
|
215
|
+
assert!(amount_optional(&ir, 0, "entities"), "the local entity is the one marked");
|
|
216
|
+
assert!(!amount_optional(&ir, 1, "value_objects"), "the domain-wide value object is left alone");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn leaves_a_field_alone_when_its_argument_is_required() {
|
|
221
|
+
let mut ir = domain(&[aggregate_with(&element(false), "", "LedgerEntry").replace("\"optional\": true", "\"optional\": false")]);
|
|
222
|
+
run(&mut ir);
|
|
223
|
+
|
|
224
|
+
assert!(!amount_optional(&ir, 0, "entities"));
|
|
225
|
+
}
|
|
226
|
+
}
|
data/rust/codegen/src/ports.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `ports.rb` — read that file's own header comments
|
|
2
2
|
//! in full; this mirrors its algorithm directly, function for function.
|
|
3
3
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
data/rust/codegen/src/queries.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `queries.rb`, mirrored function for function.
|
|
2
2
|
|
|
3
3
|
use crate::exemplar::Exemplar;
|
|
4
4
|
use crate::json::Json;
|
|
@@ -302,7 +302,7 @@ pub fn query_skip_reason(query: &Json, aggregate: &Json, value_objects_by_name:
|
|
|
302
302
|
let extra_keys = ["cursor", "consistency", "freshness", "inspection"];
|
|
303
303
|
let extras: Vec<&str> = extra_keys.iter().filter(|k| query.get(k).is_some()).copied().collect();
|
|
304
304
|
if !extras.is_empty() {
|
|
305
|
-
return Some(skip(extras[0], format!("declares {} — out of scope for this generator (rust/
|
|
305
|
+
return Some(skip(extras[0], format!("declares {} — out of scope for this generator (rust/codegen/src/queries.rs's own header has the full argument)", extras.join(", "))));
|
|
306
306
|
}
|
|
307
307
|
if query.get("index_hints").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
|
|
308
308
|
return Some(skip("index_hints", "declares use_index, out of scope for the same reason the extras above are"));
|
|
@@ -780,7 +780,7 @@ pub fn emit_query_arg_check_table(query_defs: &[QueryDef]) -> String {
|
|
|
780
780
|
})
|
|
781
781
|
.collect();
|
|
782
782
|
format!(
|
|
783
|
-
"/// C3.7 for a named query's own arguments — `query_arg_checks`\n/// (rust/
|
|
783
|
+
"/// C3.7 for a named query's own arguments — `query_arg_checks`\n/// (rust/codegen/src/queries.rs) has the full story.\npub fn check_query_args(verb: &str, args: &crate::kernel::Json) -> Result<(), crate::kernel::Refusal> {{\n match verb {{\n{}\n _ => Ok(()),\n }}\n}}\n",
|
|
784
784
|
arms.join("\n")
|
|
785
785
|
)
|
|
786
786
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Port of `
|
|
1
|
+
//! Port of the retired Ruby generator's `read_models.rb`, mirrored function for function —
|
|
2
2
|
//! read that file's own header for the full algorithm.
|
|
3
3
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
|
@@ -267,7 +267,7 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
|
|
|
267
267
|
None
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
// Mirrors `
|
|
270
|
+
// Mirrors the retired Ruby generator's `read_models.rb`'s `group_by_skip_reason`: the one
|
|
271
271
|
// shape the corpus declares — a single rootless head, group_by alone.
|
|
272
272
|
fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
|
|
273
273
|
let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Command router emitter, ported from `
|
|
1
|
+
//! Command router emitter, ported from the retired Ruby generator's `registry.rb`.
|
|
2
2
|
//! The entry structs mirror the Hashes `domain_generator.rb` accumulates while walking the IR.
|
|
3
3
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
|
@@ -144,7 +144,7 @@ pub fn emit_role_check(
|
|
|
144
144
|
))
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/// Port of `
|
|
147
|
+
/// Port of the retired Ruby generator's `registry.rb#emit_tenant_boundary_check`.
|
|
148
148
|
/// Hand-built: the target accessor differs for a single-attribute value object and a bare scalar.
|
|
149
149
|
pub fn emit_tenant_boundary_check(check: &TenantBoundaryCheck) -> String {
|
|
150
150
|
let ref_ident = naming::rust_ident_field(&check.reference_field);
|
|
@@ -740,7 +740,7 @@ pub fn emit_registry(exemplar: &Exemplar, aggregates: &[AggregateEntry]) -> Stri
|
|
|
740
740
|
format!("{header}{body}")
|
|
741
741
|
}
|
|
742
742
|
|
|
743
|
-
/// Port of `
|
|
743
|
+
/// Port of the retired Ruby generator's `registry.rb#emit_reference_table`.
|
|
744
744
|
pub fn emit_reference_table(aggregates: &[AggregateEntry]) -> String {
|
|
745
745
|
let rows: Vec<String> = aggregates
|
|
746
746
|
.iter()
|
|
@@ -760,7 +760,7 @@ pub fn emit_reference_table(aggregates: &[AggregateEntry]) -> String {
|
|
|
760
760
|
)
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
-
/// Port of `
|
|
763
|
+
/// Port of the retired Ruby generator's `registry.rb#emit_reference_lookup`.
|
|
764
764
|
pub fn emit_reference_lookup(aggregates: &[AggregateEntry]) -> String {
|
|
765
765
|
let arms: Vec<String> = aggregates
|
|
766
766
|
.iter()
|
data/rust/codegen/src/shared.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Predicates ported from `
|
|
1
|
+
//! Predicates ported from the retired Ruby generator's `mutations.rb` that `types.rs` and `json_codec.rs` share.
|
|
2
2
|
|
|
3
3
|
use crate::json::Json;
|
|
4
4
|
use std::collections::HashMap;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//! Writes each chapter's `ir.json` and `metadata.rs` sidecars.
|
|
1
|
+
//! Writes each chapter's `ir.json` and `metadata.rs` sidecars, from the IR as generated (marked).
|
|
2
|
+
|
|
3
|
+
use crate::json::{self, Json};
|
|
2
4
|
|
|
3
5
|
/// Renders `text` as a Rust string literal. Not Ruby's `String#inspect`, whose `\#{` and bare
|
|
4
6
|
/// `\uXXXX` escapes are not valid Rust.
|
|
@@ -22,22 +24,24 @@ pub fn rust_string_literal(text: &str) -> String {
|
|
|
22
24
|
out
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
/// `ir.json` is what `rust/host` reads at runtime, and `metadata.rs` embeds the same text.
|
|
28
|
+
pub fn write(mod_dir: &std::path::Path, ir: &Json, source_label: &str) -> Result<(std::path::PathBuf, std::path::PathBuf), String> {
|
|
26
29
|
std::fs::create_dir_all(mod_dir).map_err(|e| format!("creating {}: {e}", mod_dir.display()))?;
|
|
27
30
|
|
|
31
|
+
let ir_text = json::write(ir);
|
|
28
32
|
let ir_json_path = mod_dir.join("ir.json");
|
|
29
|
-
std::fs::write(&ir_json_path, ir_text).map_err(|e| format!("writing {}: {e}", ir_json_path.display()))?;
|
|
33
|
+
std::fs::write(&ir_json_path, &ir_text).map_err(|e| format!("writing {}: {e}", ir_json_path.display()))?;
|
|
30
34
|
|
|
31
35
|
let metadata_path = mod_dir.join("metadata.rs");
|
|
32
36
|
let mut contents = String::new();
|
|
33
|
-
//
|
|
37
|
+
// The header every generated `metadata.rs` carries; `regenerate_corpus --check` diffs it.
|
|
34
38
|
contents.push_str("// GENERATED by hecks project_rust — ");
|
|
35
39
|
contents.push_str(source_label);
|
|
36
40
|
contents.push_str("'s own canonical IR,\n");
|
|
37
41
|
contents.push_str("// embedded for runtime self-description. Not read by any dispatch\n");
|
|
38
42
|
contents.push_str("// function in this module — introspection only.\n");
|
|
39
43
|
contents.push_str("pub const IR_JSON: &str = ");
|
|
40
|
-
contents.push_str(&rust_string_literal(ir_text));
|
|
44
|
+
contents.push_str(&rust_string_literal(&ir_text));
|
|
41
45
|
contents.push_str(";\n");
|
|
42
46
|
std::fs::write(&metadata_path, contents).map_err(|e| format!("writing {}: {e}", metadata_path.display()))?;
|
|
43
47
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! A skip decision plus the construct family that forced it (`
|
|
1
|
+
//! A skip decision plus the construct family that forced it (the retired Ruby generator's `skip_reason.rb`).
|
|
2
2
|
//! `construct` goes to `manifest.json` beside the reason so tools read the family, not the prose.
|
|
3
3
|
|
|
4
4
|
use std::fmt;
|
data/rust/codegen/src/types.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Type emitters ported from `
|
|
1
|
+
//! Type emitters ported from the retired Ruby generator's `types.rb`: records, entities, value objects and
|
|
2
2
|
//! closed-set tables.
|
|
3
3
|
|
|
4
4
|
use crate::exemplar::Exemplar;
|
|
@@ -286,7 +286,7 @@ pub fn emit_record(exemplar: &Exemplar, aggregate: &Json, value_objects_by_name:
|
|
|
286
286
|
format!("{struct_part}\n\n{fielded_part}")
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
/// Port of `
|
|
289
|
+
/// Port of the retired Ruby generator's `types.rb#projected_field_pseudo_attributes` (ADR 0025).
|
|
290
290
|
pub fn projected_field_pseudo_attributes(aggregate: &Json) -> Vec<Json> {
|
|
291
291
|
aggregate
|
|
292
292
|
.get("projected_fields")
|
|
@@ -318,7 +318,7 @@ pub fn with_projected_field_pseudo_attributes(aggregate: &Json) -> Json {
|
|
|
318
318
|
Json::Object(pairs)
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
/// Port of `
|
|
321
|
+
/// Port of the retired Ruby generator's `types.rb#emit_set_projected_field`.
|
|
322
322
|
pub fn emit_set_projected_field(aggregate: &Json) -> String {
|
|
323
323
|
let name = naming::rust_ident(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
|
|
324
324
|
let arms: Vec<String> = aggregate
|
|
@@ -338,7 +338,7 @@ pub fn emit_set_projected_field(aggregate: &Json) -> String {
|
|
|
338
338
|
)
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
/// Port of `
|
|
341
|
+
/// Port of the retired Ruby generator's `types.rb#emit_projected_field_table`.
|
|
342
342
|
pub fn emit_projected_field_table(aggregate: &Json) -> String {
|
|
343
343
|
let name = naming::screaming_snake(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
|
|
344
344
|
let rows: Vec<String> = aggregate
|
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.0
|
data/rust/host/src/api.rs
CHANGED
|
@@ -823,7 +823,7 @@ mod tests {
|
|
|
823
823
|
}
|
|
824
824
|
|
|
825
825
|
// app.rb: `get "/api/me" { json(session[:member] || {}) }`, where
|
|
826
|
-
// session[:member] is
|
|
826
|
+
// session[:member] is the access-control module's own
|
|
827
827
|
// `{ "email" =>, "name" =>, "identity_id" =>, "role" => }`.
|
|
828
828
|
#[test]
|
|
829
829
|
fn me_is_the_ruby_engines_own_member_hash() {
|
data/rust/host/src/dispatch.rs
CHANGED
|
@@ -611,6 +611,11 @@ pub async fn query(
|
|
|
611
611
|
question: &str,
|
|
612
612
|
args: serde_json::Value,
|
|
613
613
|
) -> anyhow::Result<serde_json::Value> {
|
|
614
|
+
// A fact the query `needs` is answered before the kernel checks its arguments (ADR 0081).
|
|
615
|
+
let mut args = args;
|
|
616
|
+
if let Some(domain_ir) = crate::ir::ir() {
|
|
617
|
+
crate::needs::fill_query_needs(domain_ir, question, &mut args, &crate::needs::ProcessClock);
|
|
618
|
+
}
|
|
614
619
|
let state = read(client, wasm_path).await?;
|
|
615
620
|
let seed = state.get("instances").cloned().unwrap_or_else(|| serde_json::json!({}));
|
|
616
621
|
let input = serde_json::json!({ "seed": seed, "steps": [{ "query": question, "args": args }] }).to_string();
|