hecks 3.0.5 → 3.1.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/exe/hecks +24 -12
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +31 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +51 -29
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +11 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +52 -32
- data/lib/hecks/doors/launcher_options.rb +5 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +5 -5
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +45 -3
- data/lib/hecks/hecks/hecks.hecksagon +10 -4
- data/lib/hecks/hecks/hecks.world +6 -6
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/projections/deploy/box/settings.rb +248 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box.rb +387 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +122 -104
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- metadata +37 -1
data/rust/build/src/resolve.rs
CHANGED
|
@@ -77,12 +77,45 @@ pub fn grammar_files(root: &Path) -> Result<Vec<PathBuf>, String> {
|
|
|
77
77
|
bluebook_files(&dir)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// The names `attaches "Name"` gave, for a chapter the gem carries.
|
|
81
|
+
pub fn resolve_gem_chapters(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
|
|
82
|
+
resolve_json(parser_bin, chapter_name, hecksagon_path, "gem_chapters")
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// The names `attaches "name", from: :vendor` gave, for a vendored package.
|
|
86
|
+
pub fn resolve_vendored_packages(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
|
|
87
|
+
resolve_json(parser_bin, chapter_name, hecksagon_path, "vendored_packages")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Where the gem keeps the chapters other than framework members, relative to `lib/hecks`; the
|
|
91
|
+
// same places `Hecks::Chapters::GLOBS` lists.
|
|
92
|
+
const OTHER_GEM_CHAPTER_DIRS: &[&str] = &["language", "grammar", "tenancy/bluebook", "deploy/bluebook", "quality_control"];
|
|
93
|
+
|
|
94
|
+
// The names of the chapters the gem carries beside the framework members, found by reading each
|
|
95
|
+
// file's header. Only the Ruby runtime loads them, so the build uses this list to tell a real
|
|
96
|
+
// gem chapter from a misspelt name.
|
|
97
|
+
pub fn other_gem_chapter_names(root: &Path) -> Result<Vec<String>, String> {
|
|
98
|
+
fn walk(directory: &Path, names: &mut Vec<String>) -> Result<(), String> {
|
|
99
|
+
let Ok(entries) = std::fs::read_dir(directory) else { return Ok(()) };
|
|
100
|
+
for entry in entries {
|
|
101
|
+
let path = entry.map_err(|e| format!("reading {}: {e}", directory.display()))?.path();
|
|
102
|
+
if path.is_dir() {
|
|
103
|
+
walk(&path, names)?;
|
|
104
|
+
} else if path.extension().and_then(|e| e.to_str()) == Some("bluebook") {
|
|
105
|
+
if let Ok(name) = header_chapter_name(&path) {
|
|
106
|
+
names.push(name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
Ok(())
|
|
111
|
+
}
|
|
112
|
+
let mut names = Vec::new();
|
|
113
|
+
for directory in OTHER_GEM_CHAPTER_DIRS {
|
|
114
|
+
walk(&root.join("lib/hecks").join(directory), &mut names)?;
|
|
115
|
+
}
|
|
116
|
+
names.sort();
|
|
117
|
+
names.dedup();
|
|
118
|
+
Ok(names)
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
fn resolve_json(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path, key: &str) -> Result<Vec<String>, String> {
|
|
@@ -105,13 +138,13 @@ pub fn valid_package_name(name: &str) -> bool {
|
|
|
105
138
|
pub fn vendored_bluebook_files(domain: &Path, pkg_name: &str) -> Result<Vec<PathBuf>, String> {
|
|
106
139
|
if !valid_package_name(pkg_name) {
|
|
107
140
|
return Err(format!(
|
|
108
|
-
"
|
|
141
|
+
"attaches {pkg_name:?}, from: :vendor is not a package name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
109
142
|
));
|
|
110
143
|
}
|
|
111
144
|
let dir = domain.join("vendor").join("embryonaut_bluebooks").join(pkg_name).join("bluebook");
|
|
112
145
|
if !dir.is_dir() {
|
|
113
146
|
return Err(format!(
|
|
114
|
-
"
|
|
147
|
+
"attaches {pkg_name:?}, from: :vendor names no vendored bluebook at {} — run hecks vendor {pkg_name}",
|
|
115
148
|
dir.display()
|
|
116
149
|
));
|
|
117
150
|
}
|
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0
|
|
1
|
+
3.1.0
|
data/rust/host/src/api.rs
CHANGED
|
@@ -780,7 +780,7 @@ async fn presentation_save(
|
|
|
780
780
|
const PRESENTATION_WRITE_REFUSAL: &str =
|
|
781
781
|
"this host's kernel carries no ConsoleSettings chapter, so it has no StateStyle/Collection/Overview \
|
|
782
782
|
commands to dispatch, and writing the rows behind its back would skip the invariants those commands \
|
|
783
|
-
enforce. Attach it with `
|
|
783
|
+
enforce. Attach it with `attaches \"ConsoleSettings\"` in this domain's own .hecksagon and \
|
|
784
784
|
rebuild the kernel, or save from the Ruby console engine instead.";
|
|
785
785
|
|
|
786
786
|
// ---- response envelopes ---------------------------------------------
|
data/rust/host/src/dispatch.rs
CHANGED
|
@@ -31,7 +31,9 @@ pub const ANONYMOUS_ROLE: &str = "Anonymous";
|
|
|
31
31
|
pub enum Enforcement {
|
|
32
32
|
/// A caller with no role is unchecked, as before.
|
|
33
33
|
Off,
|
|
34
|
-
/// An unidentified or unassigned caller is let through and logged as `would_refuse_role
|
|
34
|
+
/// An unidentified or unassigned caller is let through and logged as `would_refuse_role`; a
|
|
35
|
+
/// caller that states a role is checked exactly as under `Enforce`, so shadow never accepts
|
|
36
|
+
/// what `Off` would not.
|
|
35
37
|
Shadow,
|
|
36
38
|
/// An unidentified or unassigned caller is refused.
|
|
37
39
|
Enforce,
|
|
@@ -62,6 +64,15 @@ impl Enforcement {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/// Where a dispatch comes from. A host-internal dispatch is the host acting on its own behalf (a
|
|
68
|
+
/// signup, a provisioning step, a presentation write); it names no caller and is never subject to
|
|
69
|
+
/// `HECKS_ROLE_ENFORCEMENT`, which guards what callers outside the host may do.
|
|
70
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
71
|
+
enum Origin {
|
|
72
|
+
External,
|
|
73
|
+
HostInternal,
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
/// Whether the kernel refused the command for the caller's role.
|
|
66
77
|
fn refused_for_role(result: &serde_json::Value) -> bool {
|
|
67
78
|
result.get("refusals").and_then(|r| r.as_array()).is_some_and(|all| {
|
|
@@ -189,6 +200,10 @@ pub async fn handle_facts_as(
|
|
|
189
200
|
// `role` is optional, matching `Adapters::Lambda::Client#dispatch`'s own
|
|
190
201
|
// `role: nil` default: a step with no role looks the same on the wire
|
|
191
202
|
// whether or not a caller is bound.
|
|
203
|
+
//
|
|
204
|
+
// This entry point and `handle_facts`/`handle_routed` are the host's own: a call with no role is a
|
|
205
|
+
// host-internal dispatch and is unchecked in every enforcement mode. A call that names a role is
|
|
206
|
+
// still checked against it. Callers outside the host go through the `_as` entry points.
|
|
192
207
|
pub async fn handle(
|
|
193
208
|
client: &Mutex<Client>,
|
|
194
209
|
wasm_path: &Path,
|
|
@@ -198,7 +213,8 @@ pub async fn handle(
|
|
|
198
213
|
config: &LineageConfig,
|
|
199
214
|
invoker: &dyn LambdaInvoker,
|
|
200
215
|
) -> anyhow::Result<Outcome> {
|
|
201
|
-
|
|
216
|
+
let origin = if role.is_none() { Origin::HostInternal } else { Origin::External };
|
|
217
|
+
dispatch_in(Enforcement::from_env(), origin, client, wasm_path, verb, args, Caller { role, actor_id: None }, config, invoker).await
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
/// Dispatches for a caller, applying `HECKS_ROLE_ENFORCEMENT`: unidentified callers are refused (or,
|
|
@@ -212,12 +228,13 @@ pub async fn handle_as(
|
|
|
212
228
|
config: &LineageConfig,
|
|
213
229
|
invoker: &dyn LambdaInvoker,
|
|
214
230
|
) -> anyhow::Result<Outcome> {
|
|
215
|
-
|
|
231
|
+
dispatch_in(Enforcement::from_env(), Origin::External, client, wasm_path, verb, args, caller, config, invoker).await
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
#[allow(clippy::too_many_arguments)]
|
|
219
|
-
async fn
|
|
235
|
+
async fn dispatch_in(
|
|
220
236
|
mode: Enforcement,
|
|
237
|
+
origin: Origin,
|
|
221
238
|
client: &Mutex<Client>,
|
|
222
239
|
wasm_path: &Path,
|
|
223
240
|
verb: &str,
|
|
@@ -226,9 +243,11 @@ async fn handle_with(
|
|
|
226
243
|
config: &LineageConfig,
|
|
227
244
|
invoker: &dyn LambdaInvoker,
|
|
228
245
|
) -> anyhow::Result<Outcome> {
|
|
229
|
-
let checked = mode.effective(caller);
|
|
246
|
+
let checked = if origin == Origin::HostInternal { caller } else { mode.effective(caller) };
|
|
230
247
|
let outcome = run(client, wasm_path, verb, args.clone(), checked, config, invoker).await?;
|
|
231
|
-
|
|
248
|
+
// Only a caller that stated no role (unidentified, or an actor Governance holds no grant for)
|
|
249
|
+
// is let through; a stated role that does not match stays refused, as it is under `Off`.
|
|
250
|
+
if mode == Enforcement::Shadow && caller.role.is_none() && !outcome.accepted && refused_for_role(&outcome.result) {
|
|
232
251
|
crate::log::error("would_refuse_role", serde_json::json!({
|
|
233
252
|
"verb": verb, "role": checked.role, "actor_id": checked.actor_id,
|
|
234
253
|
"refusals": outcome.result.get("refusals"),
|
|
@@ -287,6 +306,8 @@ async fn run(
|
|
|
287
306
|
let mut args = args;
|
|
288
307
|
if let Some(domain_ir) = crate::ir::ir() {
|
|
289
308
|
crate::needs::fill_needs(domain_ir, verb, &mut args, &crate::needs::ProcessClock);
|
|
309
|
+
// An argument with a declared default is filled the same way, so the journal holds it too.
|
|
310
|
+
crate::needs::fill_defaults(domain_ir, verb, &mut args);
|
|
290
311
|
}
|
|
291
312
|
let mut step = serde_json::json!({ "verb": verb, "args": args.clone() });
|
|
292
313
|
if let Some(role) = role {
|
|
@@ -330,6 +351,10 @@ async fn run(
|
|
|
330
351
|
if table.as_object().is_some_and(|t| !t.is_empty()) {
|
|
331
352
|
input["needs"] = table;
|
|
332
353
|
}
|
|
354
|
+
let defaults = crate::needs::defaults_table(domain_ir);
|
|
355
|
+
if defaults.as_object().is_some_and(|t| !t.is_empty()) {
|
|
356
|
+
input["defaults"] = defaults;
|
|
357
|
+
}
|
|
333
358
|
}
|
|
334
359
|
let input = input.to_string();
|
|
335
360
|
// `wasm_runner::run` is sync, and wasmtime-wasi's sync bridge spins up
|
|
@@ -1353,7 +1378,7 @@ pub(crate) mod tests {
|
|
|
1353
1378
|
let client = &client;
|
|
1354
1379
|
let config = &config;
|
|
1355
1380
|
async move {
|
|
1356
|
-
|
|
1381
|
+
dispatch_in(mode, Origin::External, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
|
|
1357
1382
|
.await
|
|
1358
1383
|
.unwrap()
|
|
1359
1384
|
}
|
|
@@ -1375,6 +1400,61 @@ pub(crate) mod tests {
|
|
|
1375
1400
|
assert_eq!(steps.len(), 2, "the enforced refusal was not persisted; off and shadow were");
|
|
1376
1401
|
}
|
|
1377
1402
|
|
|
1403
|
+
// Shadow may only let through what Off would: a caller that stated a wrong role is refused.
|
|
1404
|
+
#[tokio::test]
|
|
1405
|
+
async fn shadow_refuses_a_stated_wrong_role_and_still_lets_an_unidentified_caller_through() {
|
|
1406
|
+
let client = scratch_db("rust_host_dispatch_test_shadow_stated").await;
|
|
1407
|
+
provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
|
|
1408
|
+
let config = test_config("Banking", 1);
|
|
1409
|
+
let go = |mode: Enforcement, origin: Origin, caller: Caller<'static>, id: &'static str| {
|
|
1410
|
+
let client = &client;
|
|
1411
|
+
let config = &config;
|
|
1412
|
+
async move {
|
|
1413
|
+
dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), caller, config, &lambda_client::NeverInvoker)
|
|
1414
|
+
.await
|
|
1415
|
+
.unwrap()
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
let wrong = Caller { role: Some("Auditor"), actor_id: None };
|
|
1419
|
+
|
|
1420
|
+
let shadow_wrong = go(Enforcement::Shadow, Origin::External, wrong, "CUST-0031").await;
|
|
1421
|
+
assert!(!shadow_wrong.accepted, "shadow must not accept a stated wrong role: {:?}", shadow_wrong.result);
|
|
1422
|
+
assert!(refused_for_role(&shadow_wrong.result), "{:?}", shadow_wrong.result);
|
|
1423
|
+
let off_wrong = go(Enforcement::Off, Origin::External, wrong, "CUST-0032").await;
|
|
1424
|
+
assert!(!off_wrong.accepted, "off refuses it too: {:?}", off_wrong.result);
|
|
1425
|
+
|
|
1426
|
+
let nobody = go(Enforcement::Shadow, Origin::External, Caller::default(), "CUST-0033").await;
|
|
1427
|
+
assert!(nobody.accepted, "shadow lets an unidentified caller through: {:?}", nobody.result);
|
|
1428
|
+
|
|
1429
|
+
let steps = journal::load_steps(&*client.lock().await).await.unwrap();
|
|
1430
|
+
assert_eq!(steps.len(), 1, "only the unidentified caller's registration was persisted");
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// A host-internal dispatch names no caller and is unchecked in every mode, while an external
|
|
1434
|
+
// unidentified caller is refused under enforce.
|
|
1435
|
+
#[tokio::test]
|
|
1436
|
+
async fn a_host_internal_dispatch_is_unchecked_under_enforce_but_an_external_one_is_refused() {
|
|
1437
|
+
let client = scratch_db("rust_host_dispatch_test_internal").await;
|
|
1438
|
+
provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
|
|
1439
|
+
let config = test_config("Banking", 1);
|
|
1440
|
+
let go = |mode: Enforcement, origin: Origin, id: &'static str| {
|
|
1441
|
+
let client = &client;
|
|
1442
|
+
let config = &config;
|
|
1443
|
+
async move {
|
|
1444
|
+
dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
|
|
1445
|
+
.await
|
|
1446
|
+
.unwrap()
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
let external = go(Enforcement::Enforce, Origin::External, "CUST-0041").await;
|
|
1451
|
+
assert!(!external.accepted, "{:?}", external.result);
|
|
1452
|
+
for (mode, id) in [(Enforcement::Enforce, "CUST-0042"), (Enforcement::Shadow, "CUST-0043"), (Enforcement::Off, "CUST-0044")] {
|
|
1453
|
+
let internal = go(mode, Origin::HostInternal, id).await;
|
|
1454
|
+
assert!(internal.accepted, "{mode:?}: {:?}", internal.result);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1378
1458
|
#[tokio::test]
|
|
1379
1459
|
async fn a_mid_transaction_failure_rolls_back_any_saga_state_already_written() {
|
|
1380
1460
|
// Deliberately doesn't provision "Transfer": its mutation fails
|
data/rust/host/src/needs.rs
CHANGED
|
@@ -110,6 +110,52 @@ pub fn table(domain_ir: &Value) -> Value {
|
|
|
110
110
|
Value::Object(verbs)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/// The command's attributes that declare a default (`attribute :runs, Count, default: 30`), each
|
|
114
|
+
/// with the declared value. An attribute whose default is null declares none.
|
|
115
|
+
fn declared_defaults<'a>(domain_ir: &'a Value, verb: &str) -> Vec<(&'a str, &'a Value)> {
|
|
116
|
+
let Some((qualified_aggregate, command_name)) = verb.rsplit_once('.') else { return Vec::new() };
|
|
117
|
+
let aggregate = qualified_aggregate.rsplit("::").next().unwrap_or(qualified_aggregate);
|
|
118
|
+
let named = |value: &Value, name: &str| value.get("name").and_then(Value::as_str) == Some(name);
|
|
119
|
+
let list = |value: &'a Value, key: &str| -> Vec<&'a Value> {
|
|
120
|
+
value.get(key).and_then(Value::as_array).map(|a| a.iter().collect()).unwrap_or_default()
|
|
121
|
+
};
|
|
122
|
+
list(domain_ir, "aggregates")
|
|
123
|
+
.into_iter()
|
|
124
|
+
.filter(|a| named(a, aggregate))
|
|
125
|
+
.flat_map(|a| list(a, "commands"))
|
|
126
|
+
.filter(|c| named(c, command_name))
|
|
127
|
+
.flat_map(|c| list(c, "attributes"))
|
|
128
|
+
.filter_map(|a| {
|
|
129
|
+
let default = a.get("default").filter(|d| !d.is_null())?;
|
|
130
|
+
Some((a.get("name").and_then(Value::as_str)?, default))
|
|
131
|
+
})
|
|
132
|
+
.collect()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Every command's declared defaults as the kernel's input `"defaults"` reads them:
|
|
136
|
+
/// `{ verb: { attribute: value } }`. The kernel fills a reaction's command from this the same way
|
|
137
|
+
/// the host fills the outermost one.
|
|
138
|
+
pub fn defaults_table(domain_ir: &Value) -> Value {
|
|
139
|
+
let domain = domain_ir.get("name").and_then(Value::as_str).unwrap_or_default();
|
|
140
|
+
let mut verbs = serde_json::Map::new();
|
|
141
|
+
let aggregates = domain_ir.get("aggregates").and_then(Value::as_array).into_iter().flatten();
|
|
142
|
+
for aggregate in aggregates {
|
|
143
|
+
let aggregate_name = aggregate.get("name").and_then(Value::as_str).unwrap_or_default();
|
|
144
|
+
for command in aggregate.get("commands").and_then(Value::as_array).into_iter().flatten() {
|
|
145
|
+
let command_name = command.get("name").and_then(Value::as_str).unwrap_or_default();
|
|
146
|
+
let verb = format!("{domain}::{aggregate_name}.{command_name}");
|
|
147
|
+
let defaults: serde_json::Map<String, Value> = declared_defaults(domain_ir, &verb)
|
|
148
|
+
.into_iter()
|
|
149
|
+
.map(|(name, value)| (name.to_string(), value.clone()))
|
|
150
|
+
.collect();
|
|
151
|
+
if !defaults.is_empty() {
|
|
152
|
+
verbs.insert(verb, Value::Object(defaults));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
Value::Object(verbs)
|
|
157
|
+
}
|
|
158
|
+
|
|
113
159
|
/// The facts of a command invocation: the `with` object when the call carries one, else the
|
|
114
160
|
/// flat object itself (the kernel's `CommandInvocation` reads both shapes).
|
|
115
161
|
fn facts_mut(args: &mut Value) -> Option<&mut serde_json::Map<String, Value>> {
|
|
@@ -147,6 +193,22 @@ pub fn fill_needs(domain_ir: &Value, verb: &str, args: &mut Value, clock: &dyn C
|
|
|
147
193
|
}
|
|
148
194
|
}
|
|
149
195
|
|
|
196
|
+
/// Gives each argument `verb` declares a default for, and `args` leaves out, that default, after
|
|
197
|
+
/// `fill_needs` and before any gate reads the arguments. A key the caller supplied (even a null)
|
|
198
|
+
/// is kept, as is a fact `fill_needs` already answered.
|
|
199
|
+
pub fn fill_defaults(domain_ir: &Value, verb: &str, args: &mut Value) {
|
|
200
|
+
let defaults = declared_defaults(domain_ir, verb);
|
|
201
|
+
if defaults.is_empty() {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let Some(facts) = facts_mut(args) else { return };
|
|
205
|
+
for (name, value) in defaults {
|
|
206
|
+
if !facts.contains_key(name) {
|
|
207
|
+
facts.insert(name.to_string(), value.clone());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
150
212
|
#[cfg(test)]
|
|
151
213
|
mod tests {
|
|
152
214
|
use super::*;
|
|
@@ -240,4 +302,53 @@ mod tests {
|
|
|
240
302
|
clear_fixed_clock();
|
|
241
303
|
assert!(ProcessClock.now_secs() > 1_700_000_000);
|
|
242
304
|
}
|
|
305
|
+
|
|
306
|
+
// A command with one defaulted attribute, one plain, and one whose default is null.
|
|
307
|
+
fn defaulted_ir() -> Value {
|
|
308
|
+
json!({"name": "Qc", "aggregates": [{"name": "Check", "commands": [
|
|
309
|
+
{"name": "Start", "attributes": [
|
|
310
|
+
{"name": "ref", "type": "Ref", "default": null},
|
|
311
|
+
{"name": "runs", "type": "Count", "default": 30},
|
|
312
|
+
{"name": "note", "type": "String", "default": null}
|
|
313
|
+
], "needs": []}
|
|
314
|
+
]}]})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn an_omitted_argument_is_filled_with_its_declared_default() {
|
|
319
|
+
let mut args = json!({"with": {"ref": {"value": "a"}}});
|
|
320
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
321
|
+
assert_eq!(args["with"]["runs"], 30);
|
|
322
|
+
assert!(args["with"].get("note").is_none(), "a null default declares none");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn a_flat_invocation_gets_its_defaults_at_its_top_level() {
|
|
327
|
+
let mut args = json!({"ref": {"value": "a"}});
|
|
328
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
329
|
+
assert_eq!(args["runs"], 30);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn a_supplied_argument_is_kept_even_when_null() {
|
|
334
|
+
let mut args = json!({"runs": null});
|
|
335
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
336
|
+
assert!(args["runs"].is_null());
|
|
337
|
+
let mut args = json!({"runs": {"value": 7}});
|
|
338
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
339
|
+
assert_eq!(args["runs"], json!({"value": 7}));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#[test]
|
|
343
|
+
fn a_command_that_declares_no_default_is_untouched() {
|
|
344
|
+
let mut args = json!({"with": {}});
|
|
345
|
+
fill_defaults(&integer_ir(), "Qc::Check.Run", &mut args);
|
|
346
|
+
assert_eq!(args, json!({"with": {}}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn the_kernel_table_lists_each_command_that_declares_a_default() {
|
|
351
|
+
assert_eq!(defaults_table(&defaulted_ir()), json!({"Qc::Check.Start": {"runs": 30}}));
|
|
352
|
+
assert_eq!(defaults_table(&integer_ir()), json!({}));
|
|
353
|
+
}
|
|
243
354
|
}
|
data/rust/parser/src/keywords.rs
CHANGED
|
@@ -182,9 +182,9 @@ pub static KEYWORDS: &[KeywordRow] = &[
|
|
|
182
182
|
KeywordRow { word: "answers_query", context: "DomainPort", body: "none", inner: "", opens: "", fills: "answered_queries", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
183
183
|
KeywordRow { word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
184
184
|
KeywordRow { word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
185
|
-
KeywordRow { word: "
|
|
186
|
-
KeywordRow { word: "
|
|
187
|
-
KeywordRow { word: "
|
|
185
|
+
KeywordRow { word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
186
|
+
KeywordRow { word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated", was: "", resolves_via: "", disambiguator: "" },
|
|
187
|
+
KeywordRow { word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated", was: "", resolves_via: "", disambiguator: "" },
|
|
188
188
|
KeywordRow { word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
189
189
|
KeywordRow { word: "translates", context: "Hecksagon", body: "keywords", inner: "Policy", opens: "Policy", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
190
190
|
KeywordRow { word: "bounded", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
@@ -466,9 +466,10 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
466
466
|
ArgumentRow { keyword: "asks", context: "DomainPort", at: "", named: "to", kind: "constant", required: "false", fills: "to", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
467
467
|
ArgumentRow { keyword: "hecksagon", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "domain", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
468
468
|
ArgumentRow { keyword: "subscribe", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "subscriptions", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
469
|
-
ArgumentRow { keyword: "
|
|
470
|
-
ArgumentRow { keyword: "
|
|
471
|
-
ArgumentRow { keyword: "
|
|
469
|
+
ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
470
|
+
ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "", named: "from", kind: "symbol", required: "false", fills: "source", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
471
|
+
ArgumentRow { keyword: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
472
|
+
ArgumentRow { keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
472
473
|
ArgumentRow { keyword: "port", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
473
474
|
ArgumentRow { keyword: "translates", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
474
475
|
ArgumentRow { keyword: "reference_to", context: "PortOperation", at: "1", named: "", kind: "constant", required: "true", fills: "type", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
data/rust/parser/src/main.rs
CHANGED
|
@@ -91,7 +91,8 @@ fn run_chapter(args: &[String]) -> Result<(), RunError> {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/// `hecks-parse resolve --chapter <Name> <file.hecksagon>`: prints that chapter's
|
|
94
|
-
/// `
|
|
94
|
+
/// `attaches` names as JSON, in file order: `gem_chapters` for a chapter the gem carries and
|
|
95
|
+
/// `vendored_packages` for `from: :vendor`.
|
|
95
96
|
///
|
|
96
97
|
/// `--chapter` is required because one file can hold several `Hecks.hecksagon` blocks.
|
|
97
98
|
fn run_resolve(args: &[String]) -> Result<(), RunError> {
|
|
@@ -120,7 +121,7 @@ fn run_resolve(args: &[String]) -> Result<(), RunError> {
|
|
|
120
121
|
let value = emit::JsonValue::Object(vec![
|
|
121
122
|
("domain".to_string(), emit::JsonValue::String(chapter_name)),
|
|
122
123
|
(
|
|
123
|
-
"
|
|
124
|
+
"gem_chapters".to_string(),
|
|
124
125
|
emit::JsonValue::Array(
|
|
125
126
|
framework_names
|
|
126
127
|
.into_iter()
|
|
@@ -129,7 +130,7 @@ fn run_resolve(args: &[String]) -> Result<(), RunError> {
|
|
|
129
130
|
),
|
|
130
131
|
),
|
|
131
132
|
(
|
|
132
|
-
"
|
|
133
|
+
"vendored_packages".to_string(),
|
|
133
134
|
emit::JsonValue::Array(
|
|
134
135
|
vendored_names
|
|
135
136
|
.into_iter()
|
|
@@ -306,7 +306,7 @@ pub fn parse_chapter(chapter_name: &str, files: &[(String, String)]) -> ParseRes
|
|
|
306
306
|
Ok(bluebook)
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
/// Lists the chapters `<Name>`'s block pulls in via `
|
|
309
|
+
/// Lists the chapters `<Name>`'s block pulls in via `attaches` (or its deprecated spellings).
|
|
310
310
|
/// Every block is still gated; the IR accumulator is a throwaway.
|
|
311
311
|
pub fn resolve_hecksagon_dependencies(
|
|
312
312
|
chapter_name: &str,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! The `Hecksagon` construct: a `.hecksagon` file's `port` blocks and `
|
|
1
|
+
//! The `Hecksagon` construct: a `.hecksagon` file's `port` blocks and `attaches` words.
|
|
2
2
|
//! Adapter binds (`persisted_by`, `projected_by`, `subscribe`) are shape-matched and dropped.
|
|
3
3
|
|
|
4
4
|
use super::domain_port;
|
|
@@ -14,8 +14,10 @@ pub fn not_implemented(file: &str, line: usize, word: &str) -> Diagnostic {
|
|
|
14
14
|
/// Applies a `.hecksagon` body onto an already-built `ir::Bluebook`, attaching
|
|
15
15
|
/// aggregate-scoped ports to the aggregates the sibling `.bluebook` registered.
|
|
16
16
|
///
|
|
17
|
-
/// `
|
|
18
|
-
///
|
|
17
|
+
/// `gem_chapter_names` collects each `attaches "Name"` (and the deprecated `uses_framework`) and
|
|
18
|
+
/// `vendored_bluebook_names` each `attaches "name", from: :vendor` (and the deprecated
|
|
19
|
+
/// `uses_embryonaut_bluebook`), in file order, for `hecks-parse resolve`; `ir.json` itself
|
|
20
|
+
/// carries neither.
|
|
19
21
|
///
|
|
20
22
|
/// `require_matching_aggregate` is `false` for resolve, which never sees a `.bluebook`, so an
|
|
21
23
|
/// aggregate-scoped `port` skips the attach step instead of failing the lookup.
|
|
@@ -24,7 +26,7 @@ pub fn apply(
|
|
|
24
26
|
lines: &[SourceLine],
|
|
25
27
|
pos: &mut usize,
|
|
26
28
|
bluebook: &mut ir::Bluebook,
|
|
27
|
-
|
|
29
|
+
gem_chapter_names: &mut Vec<String>,
|
|
28
30
|
vendored_bluebook_names: &mut Vec<String>,
|
|
29
31
|
require_matching_aggregate: bool,
|
|
30
32
|
) -> ParseResult<()> {
|
|
@@ -96,9 +98,9 @@ pub fn apply(
|
|
|
96
98
|
let built = policy::parse_body(file, lines, pos, &name)?;
|
|
97
99
|
bluebook.policies.push(built);
|
|
98
100
|
}
|
|
99
|
-
//
|
|
101
|
+
// Deprecated spelling of `attaches "Name"`; gated, then collected.
|
|
100
102
|
"uses_framework" => {
|
|
101
|
-
|
|
103
|
+
gem_chapter_names.push(super::positional_text(
|
|
102
104
|
file,
|
|
103
105
|
gated.line.number,
|
|
104
106
|
"uses_framework",
|
|
@@ -106,7 +108,7 @@ pub fn apply(
|
|
|
106
108
|
1,
|
|
107
109
|
)?);
|
|
108
110
|
}
|
|
109
|
-
//
|
|
111
|
+
// Deprecated spelling of `attaches "name", from: :vendor`; collected the same way.
|
|
110
112
|
"uses_embryonaut_bluebook" => {
|
|
111
113
|
vendored_bluebook_names.push(super::positional_text(
|
|
112
114
|
file,
|
|
@@ -117,10 +119,23 @@ pub fn apply(
|
|
|
117
119
|
)?);
|
|
118
120
|
}
|
|
119
121
|
"subscribe" => {}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
+
// One word for a chapter the gem carries and for a vendored package; a binding
|
|
123
|
+
// fact collected for `resolve`, absent from `ir.json`. Only `from: :vendor`
|
|
124
|
+
// is admitted as a source, and anything else is refused.
|
|
122
125
|
"attaches" => {
|
|
123
|
-
|
|
126
|
+
let name =
|
|
127
|
+
super::positional_text(file, gated.line.number, "attaches", &gated.args, 1)?;
|
|
128
|
+
match super::named_symbol(&gated.args, "from").as_deref() {
|
|
129
|
+
None => gem_chapter_names.push(name),
|
|
130
|
+
Some("vendor") => vendored_bluebook_names.push(name),
|
|
131
|
+
Some(other) => {
|
|
132
|
+
return Err(Diagnostic::new(
|
|
133
|
+
file,
|
|
134
|
+
gated.line.number,
|
|
135
|
+
format!("'attaches' from: takes only :vendor, got :{other}"),
|
|
136
|
+
))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
// Consumer-owned bounded-context mark; a wiring fact, dropped like `subscribe`.
|
|
126
141
|
"bounded" => {}
|
data/rust/project/mutations.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "hecks/rust_build/append_optionals"
|
|
2
|
+
|
|
1
3
|
module RustProjection
|
|
2
4
|
module Projector
|
|
3
5
|
module_function
|
|
@@ -60,13 +62,7 @@ module RustProjection
|
|
|
60
62
|
|
|
61
63
|
# Resolves an `append` target to its entity or value object, or nil if it is neither.
|
|
62
64
|
def append_element(aggregate, target_type, value_objects_by_name)
|
|
63
|
-
|
|
64
|
-
# Local entities win over the domain-wide value objects: Syntax's local `Argument` entity
|
|
65
|
-
# shares a name with Command's `Argument` value object.
|
|
66
|
-
local = aggregate[:entities].find { |e| e[:name] == target_type }
|
|
67
|
-
return local if local
|
|
68
|
-
|
|
69
|
-
value_objects_by_name[target_type]
|
|
65
|
+
Hecks::RustBuild::AppendOptionals.element(aggregate, target_type, value_objects_by_name)
|
|
70
66
|
end
|
|
71
67
|
|
|
72
68
|
# Identity attribute and its single-field value object for an auto-minted entity element,
|
|
@@ -454,29 +450,7 @@ module RustProjection
|
|
|
454
450
|
# attribute
|
|
455
451
|
# hash once, before the emitters read it, and never resets a `true`.
|
|
456
452
|
def mark_append_optional_fields!(aggregate, value_objects_by_name)
|
|
457
|
-
aggregate
|
|
458
|
-
element = append_element(aggregate, target_attr[:type], value_objects_by_name)
|
|
459
|
-
next unless element
|
|
460
|
-
|
|
461
|
-
aggregate[:commands].each do |command|
|
|
462
|
-
command[:mutations].each do |m|
|
|
463
|
-
next unless m[:op].to_s == "append" && m[:target].to_s == target_attr[:name].to_s
|
|
464
|
-
|
|
465
|
-
m[:fields].each do |field_name, source|
|
|
466
|
-
# A Symbol is an argument name (optional ones are what this pass wants); else a
|
|
467
|
-
# literal.
|
|
468
|
-
parsed = append_field_source(source)
|
|
469
|
-
next unless parsed.is_a?(Symbol)
|
|
470
|
-
|
|
471
|
-
source_attr = command[:attributes].find { |a| a[:name].to_s == parsed.to_s }
|
|
472
|
-
next unless source_attr && source_attr[:optional]
|
|
473
|
-
|
|
474
|
-
field_attr = element[:attributes].find { |a| a[:name].to_s == field_name.to_s }
|
|
475
|
-
field_attr[:optional] = true if field_attr
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
end
|
|
479
|
-
end
|
|
453
|
+
Hecks::RustBuild::AppendOptionals.mark_aggregate(aggregate, value_objects_by_name)
|
|
480
454
|
end
|
|
481
455
|
|
|
482
456
|
# Derives the mutations of a `corrects EVENT, reverses: true` command from the commands that
|
data/rust/project/naming.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "hecks/vocabulary"
|
|
2
2
|
require "hecks/bluebook/model_check"
|
|
3
|
+
require "hecks/rust_build/domain_name"
|
|
4
|
+
require "hecks/rust_build/rust_literal"
|
|
3
5
|
|
|
4
6
|
module RustProjection
|
|
5
7
|
# Domain-to-Rust codegen support: type mapping, reserved-name checks, and
|
|
@@ -59,15 +61,10 @@ module RustProjection
|
|
|
59
61
|
# feature set.
|
|
60
62
|
#
|
|
61
63
|
# Declared once as the `CargoReservedName` vocabulary.
|
|
62
|
-
CARGO_RESERVED_DOMAIN_NAMES = Hecks::
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
def valid_domain_mod_name?(name)
|
|
67
|
-
str = name.to_s
|
|
68
|
-
str.match?(/\A[a-z_][a-z0-9_]*\z/) &&
|
|
69
|
-
Hecks::Bluebook::ModelCheck.rust_reserved_name_findings(domain_name: str).empty?
|
|
70
|
-
end
|
|
64
|
+
CARGO_RESERVED_DOMAIN_NAMES = Hecks::RustBuild::DomainName::CARGO_RESERVED
|
|
65
|
+
|
|
66
|
+
# Moved to `Hecks::RustBuild::DomainName`, which the codegen-backed `hecks project_rust` uses.
|
|
67
|
+
def valid_domain_mod_name?(name) = Hecks::RustBuild::DomainName.valid?(name)
|
|
71
68
|
|
|
72
69
|
# Identifier-shape check only (a PascalCase name always passes); whether
|
|
73
70
|
# the downcased name is a Rust keyword is `reserved_name_refusal`'s job,
|
|
@@ -220,24 +217,7 @@ module RustProjection
|
|
|
220
217
|
end
|
|
221
218
|
end
|
|
222
219
|
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
# only what Rust's string-literal grammar needs: backslash,
|
|
226
|
-
# double-quote, and control characters.
|
|
227
|
-
def rust_string_literal(str)
|
|
228
|
-
escaped = str.to_s.each_char.map do |ch|
|
|
229
|
-
case ch
|
|
230
|
-
when "\\" then "\\\\"
|
|
231
|
-
when "\"" then "\\\""
|
|
232
|
-
when "\n" then "\\n"
|
|
233
|
-
when "\r" then "\\r"
|
|
234
|
-
when "\t" then "\\t"
|
|
235
|
-
else
|
|
236
|
-
cp = ch.ord
|
|
237
|
-
cp < 0x20 || cp == 0x7F ? format("\\u{%x}", cp) : ch
|
|
238
|
-
end
|
|
239
|
-
end.join
|
|
240
|
-
"\"#{escaped}\""
|
|
241
|
-
end
|
|
220
|
+
# Moved to `Hecks::RustBuild::RustLiteral`, which the codegen-backed `hecks project_rust` uses.
|
|
221
|
+
def rust_string_literal(str) = Hecks::RustBuild::RustLiteral.string(str)
|
|
242
222
|
end
|
|
243
223
|
end
|