hecks 3.7.0 → 3.9.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/meta_validator/modes.rb +14 -0
- data/lib/hecks/bluebook/meta_validator.rb +4 -2
- data/lib/hecks/corpus/routes.rb +1 -1
- data/lib/hecks/gate/stages.yml +11 -1
- data/lib/hecks/hecks/adapters/site_toolchain/prober.rb +64 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +41 -5
- data/lib/hecks/language/bluebook/vocabulary.bluebook +1 -1
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +1 -1
- data/lib/hecks/projections/site/probe.rb +131 -0
- data/lib/hecks/projections/site/site_host/dockerfile.rb +1 -1
- data/lib/hecks/runtime/value/validation.rb +34 -3
- data/lib/hecks/site/bluebook/site.bluebook +31 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -1
- data/prebuilt/{verdicts-be73f0df9eee0abe2f4aadc968321c4b919d19d424a3ccfe1777a71dabf7d9e7.json → verdicts-3230a251570aa03666708d16f92e646feab6a3f8cc2a3540c017d748e41d41a1.json} +1 -1
- data/rust/codegen/src/fielded.rs +3 -3
- data/rust/codegen/src/json_codec.rs +2 -2
- data/rust/codegen/src/naming.rs +20 -0
- data/rust/host/Cargo.toml +4 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/commerce_ir.rs +459 -0
- data/rust/host/src/extension.rs +100 -0
- data/rust/host/src/ir.rs +0 -448
- data/rust/host/src/journal.rs +1 -1
- data/rust/host/src/lib.rs +39 -0
- data/rust/host/src/main.rs +6 -45
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +8 -8
- data/rust/host/src/payments.rs +10 -10
- data/rust/host/src/rate_limit.rs +27 -41
- data/rust/host/src/web/commerce.rs +113 -0
- data/rust/host/src/web/newsletter.rs +2 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +49 -49
- data/rust/host/src/web/resend_webhook.rs +299 -0
- data/rust/host/src/web.rs +43 -47
- metadata +12 -5
- /data/prebuilt/{945481eefbdb584649d45f256307053558966a868b9c38dee7ee88f8169610f8.marshal → 4e6c3cd1f578a38732ea5454dae52562d03d8e9886216077a5c34cbf589f3463.marshal} +0 -0
- /data/prebuilt/{a0818e2de67364dc1ab22868b6cbf869c62da6f8d9b924c778fa5cce2e76ee94.marshal → 911cdcc56b5a094ed26583a4c76401dba6455b1f434722ed22b850bc1b1bdd28.marshal} +0 -0
data/rust/host/src/ir.rs
CHANGED
|
@@ -132,300 +132,6 @@ pub fn identity_provider(domain_ir: &Value) -> Option<IdentityProvider> {
|
|
|
132
132
|
})
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/// The chapter that answers the guest newsletter signup, read from
|
|
136
|
-
/// `ir.json`'s own `newsletter` key. `None` when nothing provides it.
|
|
137
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
138
|
-
pub struct NewsletterProvider {
|
|
139
|
-
/// Chapter name, e.g. `Newsletter`.
|
|
140
|
-
pub provider: String,
|
|
141
|
-
/// Qualified subscribe command, e.g. `Newsletter::Subscriber.Subscribe`.
|
|
142
|
-
pub subscribe: String,
|
|
143
|
-
/// Qualified add-name command, e.g. `Newsletter::Subscriber.AddName`.
|
|
144
|
-
pub add_name: String,
|
|
145
|
-
/// Qualified confirm command, e.g. `Newsletter::Subscriber.Confirm`.
|
|
146
|
-
pub confirm: String,
|
|
147
|
-
/// Qualified unsubscribe command, e.g. `Newsletter::Subscriber.Unsubscribe`.
|
|
148
|
-
pub unsubscribe: String,
|
|
149
|
-
/// Qualified subscribing aggregate, e.g. `Newsletter::Subscriber`.
|
|
150
|
-
pub aggregate: String,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
impl NewsletterProvider {
|
|
154
|
-
/// The prefix every subscriber's key in a `dispatch::read` `instances`
|
|
155
|
-
/// map starts with, e.g. `Newsletter::Subscriber#`.
|
|
156
|
-
pub fn instance_prefix(&self) -> String {
|
|
157
|
-
format!("{}#", self.aggregate)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/// Whether the domain's own `aggregate`'s `command` declares an argument
|
|
162
|
-
/// named `field` — so a route only sends an argument the command declares.
|
|
163
|
-
pub fn command_declares(domain_ir: &Value, aggregate: &str, command: &str, field: &str) -> bool {
|
|
164
|
-
let named = |value: &Value, name: &str| value.get("name").and_then(|n| n.as_str()) == Some(name);
|
|
165
|
-
let Some(aggregates) = domain_ir.get("aggregates").and_then(|a| a.as_array()) else {
|
|
166
|
-
return false;
|
|
167
|
-
};
|
|
168
|
-
aggregates
|
|
169
|
-
.iter()
|
|
170
|
-
.filter(|a| named(a, aggregate))
|
|
171
|
-
.filter_map(|a| a.get("commands").and_then(|c| c.as_array()))
|
|
172
|
-
.flatten()
|
|
173
|
-
.filter(|c| named(c, command))
|
|
174
|
-
.filter_map(|c| c.get("attributes").and_then(|a| a.as_array()))
|
|
175
|
-
.flatten()
|
|
176
|
-
.any(|attribute| named(attribute, field))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
pub fn newsletter_provider(domain_ir: &Value) -> Option<NewsletterProvider> {
|
|
180
|
-
let fact = domain_ir.get("newsletter")?;
|
|
181
|
-
Some(NewsletterProvider {
|
|
182
|
-
provider: fact.get("provider")?.as_str()?.to_string(),
|
|
183
|
-
subscribe: fact.get("subscribe")?.as_str()?.to_string(),
|
|
184
|
-
add_name: fact.get("add_name")?.as_str()?.to_string(),
|
|
185
|
-
confirm: fact.get("confirm")?.as_str()?.to_string(),
|
|
186
|
-
unsubscribe: fact.get("unsubscribe")?.as_str()?.to_string(),
|
|
187
|
-
aggregate: fact.get("aggregate")?.as_str()?.to_string(),
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/// The chapter that declares `provides "newsletter_issues"`, read from
|
|
192
|
-
/// `ir.json`'s own `newsletter_issues` key. Omitted when nothing declares it.
|
|
193
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
194
|
-
pub struct NewsletterIssuesProvider {
|
|
195
|
-
/// Qualified send command, e.g. `Newsletter::Issue.Send`.
|
|
196
|
-
pub send_issue: String,
|
|
197
|
-
/// Qualified delivery-record command, e.g. `Newsletter::Delivery.Record`.
|
|
198
|
-
pub record_delivery: String,
|
|
199
|
-
/// Qualified issue aggregate, e.g. `Newsletter::Issue`.
|
|
200
|
-
pub issue_aggregate: String,
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
impl NewsletterIssuesProvider {
|
|
204
|
-
/// The prefix every issue's key in a `dispatch::read` `instances` map
|
|
205
|
-
/// starts with, e.g. `Newsletter::Issue#`.
|
|
206
|
-
pub fn issue_prefix(&self) -> String {
|
|
207
|
-
format!("{}#", self.issue_aggregate)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/// The declared issue-sending verbs, or `None` when the IR has no
|
|
212
|
-
/// `newsletter_issues` key.
|
|
213
|
-
pub fn newsletter_issues_provider(domain_ir: &Value) -> Option<NewsletterIssuesProvider> {
|
|
214
|
-
let fact = domain_ir.get("newsletter_issues")?;
|
|
215
|
-
Some(NewsletterIssuesProvider {
|
|
216
|
-
send_issue: fact.get("send_issue")?.as_str()?.to_string(),
|
|
217
|
-
record_delivery: fact.get("record_delivery")?.as_str()?.to_string(),
|
|
218
|
-
issue_aggregate: fact.get("issue_aggregate")?.as_str()?.to_string(),
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
/// The chapter that takes payments, read from `ir.json`'s own `payments`
|
|
224
|
-
/// key. `None` when nothing provides it.
|
|
225
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
226
|
-
pub struct PaymentsProvider {
|
|
227
|
-
/// Chapter name, e.g. `Payments`.
|
|
228
|
-
pub provider: String,
|
|
229
|
-
/// Qualified initiate command, e.g. `Payments::Payment.Initiate`.
|
|
230
|
-
pub initiate: String,
|
|
231
|
-
/// Qualified port operation the processor's success report dispatches,
|
|
232
|
-
/// e.g. `Payments::Payment.PaymentGateway.Succeeded`.
|
|
233
|
-
pub succeeded: String,
|
|
234
|
-
/// Qualified port operation the processor's failure report dispatches,
|
|
235
|
-
/// e.g. `Payments::Payment.PaymentGateway.Failed`.
|
|
236
|
-
pub failed: String,
|
|
237
|
-
/// Qualified paying aggregate, e.g. `Payments::Payment`.
|
|
238
|
-
pub aggregate: String,
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
impl PaymentsProvider {
|
|
242
|
-
/// The prefix every payment's key in a `dispatch::read` `instances` map
|
|
243
|
-
/// starts with, e.g. `Payments::Payment#`.
|
|
244
|
-
pub fn instance_prefix(&self) -> String {
|
|
245
|
-
format!("{}#", self.aggregate)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
pub fn payments_provider(domain_ir: &Value) -> Option<PaymentsProvider> {
|
|
250
|
-
let fact = domain_ir.get("payments")?;
|
|
251
|
-
Some(PaymentsProvider {
|
|
252
|
-
provider: fact.get("provider")?.as_str()?.to_string(),
|
|
253
|
-
initiate: fact.get("initiate")?.as_str()?.to_string(),
|
|
254
|
-
succeeded: fact.get("succeeded")?.as_str()?.to_string(),
|
|
255
|
-
failed: fact.get("failed")?.as_str()?.to_string(),
|
|
256
|
-
aggregate: fact.get("aggregate")?.as_str()?.to_string(),
|
|
257
|
-
})
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/// The chapter that answers scheduling sessions and taking registrations,
|
|
261
|
-
/// read from `ir.json`'s own `registrations` key. `None` when nothing provides it.
|
|
262
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
263
|
-
pub struct RegistrationsProvider {
|
|
264
|
-
/// Chapter name, e.g. `Studio`.
|
|
265
|
-
pub provider: String,
|
|
266
|
-
/// Qualified schedule command, e.g. `Studio::Event.Schedule`.
|
|
267
|
-
pub schedule: String,
|
|
268
|
-
/// Qualified request command, e.g. `Studio::Registration.Request`.
|
|
269
|
-
pub request: String,
|
|
270
|
-
/// Qualified event aggregate, e.g. `Studio::Event`.
|
|
271
|
-
pub event_aggregate: String,
|
|
272
|
-
/// Qualified registration aggregate, e.g. `Studio::Registration`.
|
|
273
|
-
pub registration_aggregate: String,
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
impl RegistrationsProvider {
|
|
277
|
-
/// The conventional names for a domain that declares no
|
|
278
|
-
/// `provides "registrations"` — `Event`/`Registration` under the domain itself.
|
|
279
|
-
pub fn conventional(domain: &str) -> Self {
|
|
280
|
-
Self {
|
|
281
|
-
provider: domain.to_string(),
|
|
282
|
-
schedule: format!("{domain}::Event.Schedule"),
|
|
283
|
-
request: format!("{domain}::Registration.Request"),
|
|
284
|
-
event_aggregate: format!("{domain}::Event"),
|
|
285
|
-
registration_aggregate: format!("{domain}::Registration"),
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/// The prefix every event's key in a `dispatch::read` `instances` map
|
|
290
|
-
/// starts with, e.g. `Studio::Event#`.
|
|
291
|
-
pub fn event_prefix(&self) -> String {
|
|
292
|
-
format!("{}#", self.event_aggregate)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/// The prefix every registration's key starts with, e.g.
|
|
296
|
-
/// `Studio::Registration#`.
|
|
297
|
-
pub fn registration_prefix(&self) -> String {
|
|
298
|
-
format!("{}#", self.registration_aggregate)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/// The request command's own aggregate and command name without the
|
|
302
|
-
/// chapter qualifier, e.g. `("Registration", "Request")`, as `command_declares` expects.
|
|
303
|
-
pub fn request_target(&self) -> Option<(&str, &str)> {
|
|
304
|
-
self.request.rsplit("::").next()?.split_once('.')
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
pub fn registrations_provider(domain_ir: &Value) -> Option<RegistrationsProvider> {
|
|
309
|
-
let fact = domain_ir.get("registrations")?;
|
|
310
|
-
Some(RegistrationsProvider {
|
|
311
|
-
provider: fact.get("provider")?.as_str()?.to_string(),
|
|
312
|
-
schedule: fact.get("schedule")?.as_str()?.to_string(),
|
|
313
|
-
request: fact.get("request")?.as_str()?.to_string(),
|
|
314
|
-
event_aggregate: fact.get("event_aggregate")?.as_str()?.to_string(),
|
|
315
|
-
registration_aggregate: fact.get("registration_aggregate")?.as_str()?.to_string(),
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/// The registrations binding of the IR this host loaded, or the
|
|
320
|
-
/// conventional names for `domain` when the IR declares none.
|
|
321
|
-
pub fn registrations_binding(domain: &str) -> RegistrationsProvider {
|
|
322
|
-
ir().and_then(registrations_provider).unwrap_or_else(|| RegistrationsProvider::conventional(domain))
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/// One command of the payment-processor connection, by the role it plays —
|
|
326
|
-
/// resolved through `PaymentConnectionProvider::verb`.
|
|
327
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
328
|
-
pub enum ConnectionVerb {
|
|
329
|
-
Connect,
|
|
330
|
-
Reconnect,
|
|
331
|
-
Disconnect,
|
|
332
|
-
Suspend,
|
|
333
|
-
Resume,
|
|
334
|
-
Enable,
|
|
335
|
-
Disable,
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/// The chapter that owns the business's payment-processor connection, read
|
|
339
|
-
/// from `ir.json`'s own `payment_connection` key. `None` when nothing provides it.
|
|
340
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
341
|
-
pub struct PaymentConnectionProvider {
|
|
342
|
-
/// Chapter name, e.g. `Studio`.
|
|
343
|
-
pub provider: String,
|
|
344
|
-
pub connect: String,
|
|
345
|
-
pub reconnect: String,
|
|
346
|
-
pub disconnect: String,
|
|
347
|
-
pub suspend: String,
|
|
348
|
-
pub resume: String,
|
|
349
|
-
pub enable: String,
|
|
350
|
-
pub disable: String,
|
|
351
|
-
/// Qualified connection aggregate, e.g. `Studio::PaymentConnection`.
|
|
352
|
-
pub aggregate: String,
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
impl PaymentConnectionProvider {
|
|
356
|
-
/// The conventional names for a domain that declares no
|
|
357
|
-
/// `provides "payment_connection"` — `PaymentConnection` under the domain itself.
|
|
358
|
-
pub fn conventional(domain: &str) -> Self {
|
|
359
|
-
let qualified = |command: &str| format!("{domain}::PaymentConnection.{command}");
|
|
360
|
-
Self {
|
|
361
|
-
provider: domain.to_string(),
|
|
362
|
-
connect: qualified("Connect"),
|
|
363
|
-
reconnect: qualified("Reconnect"),
|
|
364
|
-
disconnect: qualified("Disconnect"),
|
|
365
|
-
suspend: qualified("Suspend"),
|
|
366
|
-
resume: qualified("Resume"),
|
|
367
|
-
enable: qualified("EnablePayments"),
|
|
368
|
-
disable: qualified("DisablePayments"),
|
|
369
|
-
aggregate: format!("{domain}::PaymentConnection"),
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/// The prefix every connection's key in a `dispatch::read` `instances`
|
|
374
|
-
/// map starts with, e.g. `Studio::PaymentConnection#`.
|
|
375
|
-
pub fn instance_prefix(&self) -> String {
|
|
376
|
-
format!("{}#", self.aggregate)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/// The qualified command that plays `which`.
|
|
380
|
-
pub fn verb(&self, which: ConnectionVerb) -> &str {
|
|
381
|
-
match which {
|
|
382
|
-
ConnectionVerb::Connect => &self.connect,
|
|
383
|
-
ConnectionVerb::Reconnect => &self.reconnect,
|
|
384
|
-
ConnectionVerb::Disconnect => &self.disconnect,
|
|
385
|
-
ConnectionVerb::Suspend => &self.suspend,
|
|
386
|
-
ConnectionVerb::Resume => &self.resume,
|
|
387
|
-
ConnectionVerb::Enable => &self.enable,
|
|
388
|
-
ConnectionVerb::Disable => &self.disable,
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
pub fn payment_connection_provider(domain_ir: &Value) -> Option<PaymentConnectionProvider> {
|
|
394
|
-
let fact = domain_ir.get("payment_connection")?;
|
|
395
|
-
let text = |key: &str| Some(fact.get(key)?.as_str()?.to_string());
|
|
396
|
-
Some(PaymentConnectionProvider {
|
|
397
|
-
provider: text("provider")?,
|
|
398
|
-
connect: text("connect")?,
|
|
399
|
-
reconnect: text("reconnect")?,
|
|
400
|
-
disconnect: text("disconnect")?,
|
|
401
|
-
suspend: text("suspend")?,
|
|
402
|
-
resume: text("resume")?,
|
|
403
|
-
enable: text("enable")?,
|
|
404
|
-
disable: text("disable")?,
|
|
405
|
-
aggregate: text("aggregate")?,
|
|
406
|
-
})
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/// The payment-connection binding of the IR this host loaded, or the
|
|
410
|
-
/// conventional names for `domain` when the IR declares none.
|
|
411
|
-
pub fn payment_connection_binding(domain: &str) -> PaymentConnectionProvider {
|
|
412
|
-
ir().and_then(payment_connection_provider).unwrap_or_else(|| PaymentConnectionProvider::conventional(domain))
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/// The checkout fixture's own payments binding, read from its committed
|
|
416
|
-
/// `ir.json` — what the checkout routes' tests dispatch against.
|
|
417
|
-
#[cfg(test)]
|
|
418
|
-
pub fn fixture_ir() -> Value {
|
|
419
|
-
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/generated/checkout_fixture/ir.json");
|
|
420
|
-
serde_json::from_str(&std::fs::read_to_string(path).expect("checkout_fixture ir.json")).expect("valid json")
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/// The checkout fixture's own payments binding.
|
|
424
|
-
#[cfg(test)]
|
|
425
|
-
pub fn fixture_payments() -> PaymentsProvider {
|
|
426
|
-
payments_provider(&fixture_ir()).expect("the checkout fixture provides payments")
|
|
427
|
-
}
|
|
428
|
-
|
|
429
135
|
/// Refuses at boot rather than silently building a second, disjoint history
|
|
430
136
|
/// nothing but this runtime reads while the real state lives elsewhere.
|
|
431
137
|
pub fn refuse_unsupported_persistence_adapters(domain_ir: &Value) -> Result<(), String> {
|
|
@@ -570,160 +276,6 @@ mod tests {
|
|
|
570
276
|
assert!(identity_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
|
|
571
277
|
}
|
|
572
278
|
|
|
573
|
-
#[test]
|
|
574
|
-
fn newsletter_provider_reads_the_declared_capability() {
|
|
575
|
-
let ir = serde_json::json!({
|
|
576
|
-
"name": "Studio",
|
|
577
|
-
"newsletter": {
|
|
578
|
-
"provider": "Newsletter",
|
|
579
|
-
"subscribe": "Newsletter::Subscriber.Subscribe",
|
|
580
|
-
"add_name": "Newsletter::Subscriber.AddName",
|
|
581
|
-
"confirm": "Newsletter::Subscriber.Confirm",
|
|
582
|
-
"unsubscribe": "Newsletter::Subscriber.Unsubscribe",
|
|
583
|
-
"aggregate": "Newsletter::Subscriber"
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
let provider = newsletter_provider(&ir).expect("should find newsletter");
|
|
587
|
-
assert_eq!(provider.provider, "Newsletter");
|
|
588
|
-
assert_eq!(provider.subscribe, "Newsletter::Subscriber.Subscribe");
|
|
589
|
-
assert_eq!(provider.add_name, "Newsletter::Subscriber.AddName");
|
|
590
|
-
assert_eq!(provider.confirm, "Newsletter::Subscriber.Confirm");
|
|
591
|
-
assert_eq!(provider.unsubscribe, "Newsletter::Subscriber.Unsubscribe");
|
|
592
|
-
assert_eq!(provider.instance_prefix(), "Newsletter::Subscriber#");
|
|
593
|
-
assert!(newsletter_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
#[test]
|
|
597
|
-
fn newsletter_issues_provider_reads_the_declared_capability() {
|
|
598
|
-
let ir = serde_json::json!({
|
|
599
|
-
"name": "Studio",
|
|
600
|
-
"newsletter_issues": {
|
|
601
|
-
"provider": "Newsletter",
|
|
602
|
-
"send_issue": "Newsletter::Issue.Send",
|
|
603
|
-
"record_delivery": "Newsletter::Delivery.Record",
|
|
604
|
-
"issue_aggregate": "Newsletter::Issue",
|
|
605
|
-
"delivery_aggregate": "Newsletter::Delivery"
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
let provider = newsletter_issues_provider(&ir).expect("should find newsletter_issues");
|
|
609
|
-
assert_eq!(provider.send_issue, "Newsletter::Issue.Send");
|
|
610
|
-
assert_eq!(provider.record_delivery, "Newsletter::Delivery.Record");
|
|
611
|
-
assert_eq!(provider.issue_prefix(), "Newsletter::Issue#");
|
|
612
|
-
assert!(newsletter_issues_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
#[test]
|
|
616
|
-
fn payments_provider_reads_the_declared_capability() {
|
|
617
|
-
let ir = serde_json::json!({
|
|
618
|
-
"name": "Studio",
|
|
619
|
-
"payments": {
|
|
620
|
-
"provider": "Payments",
|
|
621
|
-
"initiate": "Payments::Payment.Initiate",
|
|
622
|
-
"succeeded": "Payments::Payment.PaymentGateway.Succeeded",
|
|
623
|
-
"failed": "Payments::Payment.PaymentGateway.Failed",
|
|
624
|
-
"aggregate": "Payments::Payment"
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
let provider = payments_provider(&ir).expect("should find payments");
|
|
628
|
-
assert_eq!(provider.provider, "Payments");
|
|
629
|
-
assert_eq!(provider.initiate, "Payments::Payment.Initiate");
|
|
630
|
-
assert_eq!(provider.succeeded, "Payments::Payment.PaymentGateway.Succeeded");
|
|
631
|
-
assert_eq!(provider.failed, "Payments::Payment.PaymentGateway.Failed");
|
|
632
|
-
assert_eq!(provider.instance_prefix(), "Payments::Payment#");
|
|
633
|
-
assert!(payments_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
#[test]
|
|
637
|
-
fn registrations_provider_reads_the_declared_capability() {
|
|
638
|
-
let provider = registrations_provider(&fixture_ir()).expect("the checkout fixture provides registrations");
|
|
639
|
-
assert_eq!(provider.schedule, "CheckoutFixture::Event.Schedule");
|
|
640
|
-
assert_eq!(provider.request, "CheckoutFixture::Registration.Request");
|
|
641
|
-
assert_eq!(provider.event_prefix(), "CheckoutFixture::Event#");
|
|
642
|
-
assert_eq!(provider.registration_prefix(), "CheckoutFixture::Registration#");
|
|
643
|
-
assert_eq!(provider.request_target(), Some(("Registration", "Request")));
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
#[test]
|
|
647
|
-
fn the_conventional_registrations_names_match_what_the_fixture_declares() {
|
|
648
|
-
assert_eq!(registrations_provider(&fixture_ir()), Some(RegistrationsProvider::conventional("CheckoutFixture")));
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
#[test]
|
|
652
|
-
fn a_declared_registrations_binding_names_whatever_the_chapter_calls_things() {
|
|
653
|
-
let ir = serde_json::json!({"registrations": {
|
|
654
|
-
"provider": "Bookings",
|
|
655
|
-
"schedule": "Bookings::Session.Plan",
|
|
656
|
-
"request": "Bookings::Seat.Claim",
|
|
657
|
-
"event_aggregate": "Bookings::Session",
|
|
658
|
-
"registration_aggregate": "Bookings::Seat"
|
|
659
|
-
}});
|
|
660
|
-
let provider = registrations_provider(&ir).expect("declared");
|
|
661
|
-
assert_eq!(provider.event_prefix(), "Bookings::Session#");
|
|
662
|
-
assert_eq!(provider.registration_prefix(), "Bookings::Seat#");
|
|
663
|
-
assert_eq!(provider.request_target(), Some(("Seat", "Claim")));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
#[test]
|
|
667
|
-
fn registrations_provider_is_none_without_the_key() {
|
|
668
|
-
assert_eq!(registrations_provider(&serde_json::json!({"name": "Pizzas"})), None);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
#[test]
|
|
672
|
-
fn registrations_binding_falls_back_to_the_conventional_names_when_no_ir_is_loaded() {
|
|
673
|
-
assert_eq!(registrations_binding("Shop"), RegistrationsProvider::conventional("Shop"));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
#[test]
|
|
677
|
-
fn payment_connection_provider_reads_the_declared_capability() {
|
|
678
|
-
let provider = payment_connection_provider(&fixture_ir()).expect("the checkout fixture provides payment_connection");
|
|
679
|
-
assert_eq!(provider.verb(ConnectionVerb::Connect), "CheckoutFixture::PaymentConnection.Connect");
|
|
680
|
-
assert_eq!(provider.verb(ConnectionVerb::Enable), "CheckoutFixture::PaymentConnection.EnablePayments");
|
|
681
|
-
assert_eq!(provider.verb(ConnectionVerb::Disable), "CheckoutFixture::PaymentConnection.DisablePayments");
|
|
682
|
-
assert_eq!(provider.instance_prefix(), "CheckoutFixture::PaymentConnection#");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
#[test]
|
|
686
|
-
fn the_conventional_payment_connection_names_match_what_the_fixture_declares() {
|
|
687
|
-
assert_eq!(payment_connection_provider(&fixture_ir()), Some(PaymentConnectionProvider::conventional("CheckoutFixture")));
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
#[test]
|
|
691
|
-
fn a_declared_payment_connection_binding_names_whatever_the_chapter_calls_things() {
|
|
692
|
-
let ir = serde_json::json!({"payment_connection": {
|
|
693
|
-
"provider": "Billing",
|
|
694
|
-
"connect": "Billing::Link.Open", "reconnect": "Billing::Link.Reopen",
|
|
695
|
-
"disconnect": "Billing::Link.Close", "suspend": "Billing::Link.Pause",
|
|
696
|
-
"resume": "Billing::Link.Unpause", "enable": "Billing::Link.TurnOn",
|
|
697
|
-
"disable": "Billing::Link.TurnOff", "aggregate": "Billing::Link"
|
|
698
|
-
}});
|
|
699
|
-
let provider = payment_connection_provider(&ir).expect("declared");
|
|
700
|
-
assert_eq!(provider.verb(ConnectionVerb::Suspend), "Billing::Link.Pause");
|
|
701
|
-
assert_eq!(provider.verb(ConnectionVerb::Resume), "Billing::Link.Unpause");
|
|
702
|
-
assert_eq!(provider.instance_prefix(), "Billing::Link#");
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
#[test]
|
|
706
|
-
fn payment_connection_binding_falls_back_to_the_conventional_names_when_no_ir_is_loaded() {
|
|
707
|
-
assert_eq!(payment_connection_binding("Shop"), PaymentConnectionProvider::conventional("Shop"));
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
#[test]
|
|
711
|
-
fn command_declares_finds_an_argument_on_the_named_command_only() {
|
|
712
|
-
let ir = serde_json::json!({
|
|
713
|
-
"aggregates": [
|
|
714
|
-
{"name": "Registration", "commands": [
|
|
715
|
-
{"name": "Request", "attributes": [{"name": "email"}, {"name": "news_signup"}]},
|
|
716
|
-
{"name": "Cancel", "attributes": [{"name": "reason"}]}
|
|
717
|
-
]},
|
|
718
|
-
{"name": "Event", "commands": [{"name": "Request", "attributes": [{"name": "slug"}]}]}
|
|
719
|
-
]
|
|
720
|
-
});
|
|
721
|
-
assert!(command_declares(&ir, "Registration", "Request", "news_signup"));
|
|
722
|
-
assert!(!command_declares(&ir, "Registration", "Cancel", "news_signup"));
|
|
723
|
-
assert!(!command_declares(&ir, "Event", "Request", "news_signup"));
|
|
724
|
-
assert!(!command_declares(&ir, "Registration", "Request", "phone"));
|
|
725
|
-
assert!(!command_declares(&serde_json::json!({"name": "Pizzas"}), "Registration", "Request", "news_signup"));
|
|
726
|
-
}
|
|
727
279
|
|
|
728
280
|
#[test]
|
|
729
281
|
fn refuse_unsupported_persistence_adapters_passes_a_domain_with_no_persistence_key() {
|
data/rust/host/src/journal.rs
CHANGED
|
@@ -334,7 +334,7 @@ fn storage_name(qualified_aggregate: &str) -> String {
|
|
|
334
334
|
|
|
335
335
|
/// Quotes a Postgres identifier `PG::Connection.quote_ident`-style —
|
|
336
336
|
/// needed since table names can't be bound as query parameters.
|
|
337
|
-
pub
|
|
337
|
+
pub fn quote_ident(name: &str) -> String {
|
|
338
338
|
format!("\"{}\"", name.replace('"', "\"\""))
|
|
339
339
|
}
|
|
340
340
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//! The host as a library: the modules the `bootstrap` binary boots, plus the harness binaries' shared code.
|
|
2
|
+
//! Commerce (payments, checkout, newsletter, registrations, mail) plugs in through [`extension::HostExtension`].
|
|
3
|
+
|
|
4
|
+
pub mod api;
|
|
5
|
+
pub mod approval;
|
|
6
|
+
pub mod auth;
|
|
7
|
+
pub mod checkout;
|
|
8
|
+
pub mod commerce_ir;
|
|
9
|
+
pub mod dispatch;
|
|
10
|
+
pub mod expr_json;
|
|
11
|
+
pub mod extension;
|
|
12
|
+
pub mod field_hints;
|
|
13
|
+
#[cfg(test)]
|
|
14
|
+
pub mod fuzz_support;
|
|
15
|
+
#[cfg(test)]
|
|
16
|
+
#[path = "boundary_fuzz/expr_json.rs"]
|
|
17
|
+
pub mod expr_json_fuzz;
|
|
18
|
+
pub mod ir;
|
|
19
|
+
pub mod journal;
|
|
20
|
+
pub mod lambda_client;
|
|
21
|
+
pub mod log;
|
|
22
|
+
pub mod mint;
|
|
23
|
+
pub mod needs;
|
|
24
|
+
pub mod payments;
|
|
25
|
+
pub mod presentation;
|
|
26
|
+
pub mod presentation_write;
|
|
27
|
+
pub mod query_step;
|
|
28
|
+
pub mod rate_limit;
|
|
29
|
+
pub mod reference_transform;
|
|
30
|
+
pub mod reference_validate;
|
|
31
|
+
pub mod resend;
|
|
32
|
+
pub mod secrets;
|
|
33
|
+
pub mod server;
|
|
34
|
+
pub mod storage_shape;
|
|
35
|
+
#[cfg(test)]
|
|
36
|
+
pub mod test_pg;
|
|
37
|
+
pub mod ui_schema;
|
|
38
|
+
pub mod wasm_runner;
|
|
39
|
+
pub mod web;
|
data/rust/host/src/main.rs
CHANGED
|
@@ -1,40 +1,7 @@
|
|
|
1
1
|
//! Lambda custom-runtime entry point (or a long-lived server under
|
|
2
2
|
//! HECKS_SERVE_MODE=1): boots Postgres/wasm state, then dispatches events.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
mod approval;
|
|
6
|
-
mod auth;
|
|
7
|
-
mod checkout;
|
|
8
|
-
mod dispatch;
|
|
9
|
-
mod expr_json;
|
|
10
|
-
mod field_hints;
|
|
11
|
-
#[cfg(test)]
|
|
12
|
-
mod fuzz_support;
|
|
13
|
-
#[cfg(test)]
|
|
14
|
-
#[path = "boundary_fuzz/expr_json.rs"]
|
|
15
|
-
mod expr_json_fuzz;
|
|
16
|
-
mod ir;
|
|
17
|
-
mod journal;
|
|
18
|
-
mod lambda_client;
|
|
19
|
-
mod log;
|
|
20
|
-
mod mint;
|
|
21
|
-
mod needs;
|
|
22
|
-
mod payments;
|
|
23
|
-
mod presentation;
|
|
24
|
-
mod presentation_write;
|
|
25
|
-
mod query_step;
|
|
26
|
-
mod rate_limit;
|
|
27
|
-
mod reference_transform;
|
|
28
|
-
mod reference_validate;
|
|
29
|
-
mod resend;
|
|
30
|
-
mod secrets;
|
|
31
|
-
mod server;
|
|
32
|
-
mod storage_shape;
|
|
33
|
-
#[cfg(test)]
|
|
34
|
-
mod test_pg;
|
|
35
|
-
mod ui_schema;
|
|
36
|
-
mod wasm_runner;
|
|
37
|
-
mod web;
|
|
4
|
+
use rust_host::{approval, auth, extension, ir, journal, lambda_client, log, mint, rate_limit, secrets, server, storage_shape};
|
|
38
5
|
|
|
39
6
|
use lambda_runtime::{service_fn, Error, LambdaEvent};
|
|
40
7
|
use std::path::PathBuf;
|
|
@@ -113,16 +80,8 @@ async fn main() -> Result<(), Error> {
|
|
|
113
80
|
std::env::set_var("SESSION_SECRET", session_secret);
|
|
114
81
|
}
|
|
115
82
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// host from serving everything else (resend.rs answers 503 without
|
|
119
|
-
// a key).
|
|
120
|
-
if let Ok(resend_secret_id) = std::env::var("RESEND_SECRET_ID") {
|
|
121
|
-
let fetched = secret_fetcher.fetch_secret_string(&resend_secret_id).await.map_err(|e| format!("{e:#}"));
|
|
122
|
-
match fetched.and_then(|json| secrets::extract_field(&json, "api_key")) {
|
|
123
|
-
Ok(api_key) => unsafe { std::env::set_var("RESEND_API_KEY", api_key) },
|
|
124
|
-
Err(e) => eprintln!("RESEND_SECRET_ID could not be read, so email sending stays off: {e}"),
|
|
125
|
-
}
|
|
83
|
+
for extension in extension::installed() {
|
|
84
|
+
extension.boot_secrets(&secret_fetcher).await;
|
|
126
85
|
}
|
|
127
86
|
let wasm_path = PathBuf::from(
|
|
128
87
|
std::env::var("HECKS_WASM_PATH").unwrap_or_else(|_| "banking.wasm".to_string()),
|
|
@@ -137,7 +96,9 @@ async fn main() -> Result<(), Error> {
|
|
|
137
96
|
let schema = std::env::var("HECKS_SCHEMA").ok().filter(|s| !s.is_empty());
|
|
138
97
|
// Checkout on AWS keeps the business's payment keys in a named Secrets
|
|
139
98
|
// Manager secret; refuse before touching the database when none is named.
|
|
140
|
-
|
|
99
|
+
for extension in extension::installed() {
|
|
100
|
+
extension.boot_check(&domain)?;
|
|
101
|
+
}
|
|
141
102
|
|
|
142
103
|
// RDS refuses a plain NoTls connection by default and needs AWS's
|
|
143
104
|
// own RDS CA specifically, not a generic public bundle (see
|
data/rust/host/src/mint.rs
CHANGED
|
@@ -495,7 +495,7 @@ async fn table_exists<C: GenericClient>(client: &C, name: &str) -> anyhow::Resul
|
|
|
495
495
|
|
|
496
496
|
/// No backfill: adopting an era means its history already exists elsewhere.
|
|
497
497
|
/// Runs in its own transaction because `create_head_snapshot`'s SAVEPOINT needs one open.
|
|
498
|
-
pub
|
|
498
|
+
pub async fn adopt_head_snapshots<C: GenericClient>(
|
|
499
499
|
client: &C,
|
|
500
500
|
domain: &str,
|
|
501
501
|
aggregates: &[Aggregate],
|
|
@@ -300,7 +300,7 @@ impl Tenant {
|
|
|
300
300
|
async fn register(&self, slug: &str) -> (u64, Value) {
|
|
301
301
|
let body = json!({"event_slug": slug, "name": "Ada Lovelace", "email": "ada@example.com"}).to_string();
|
|
302
302
|
let platform = self.platform_now().await;
|
|
303
|
-
let response = web::registrations_route(&body, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::
|
|
303
|
+
let response = web::registrations_route(&body, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
|
|
304
304
|
(response["statusCode"].as_u64().unwrap(), serde_json::from_str(response["body"].as_str().unwrap()).unwrap_or(Value::Null))
|
|
305
305
|
}
|
|
306
306
|
|
|
@@ -314,14 +314,14 @@ impl Tenant {
|
|
|
314
314
|
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
|
|
315
315
|
let header = sign(secret, now, &payload);
|
|
316
316
|
let platform = self.platform_now().await;
|
|
317
|
-
let response = web::webhook_route(&payload, &header, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::
|
|
317
|
+
let response = web::webhook_route(&payload, &header, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
|
|
318
318
|
response["statusCode"].as_u64().unwrap()
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// POST /registrations/:id/complete with a successful outcome (the mock
|
|
322
322
|
// walkthrough's "Pay" button).
|
|
323
323
|
async fn settle(&self, reference: &str) -> u64 {
|
|
324
|
-
let response = web::registration_complete_route(reference, r#"{"outcome":"succeeded"}"#, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::
|
|
324
|
+
let response = web::registration_complete_route(reference, r#"{"outcome":"succeeded"}"#, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
|
|
325
325
|
response["statusCode"].as_u64().unwrap()
|
|
326
326
|
}
|
|
327
327
|
|
|
@@ -672,7 +672,7 @@ async fn a_declined_payment_frees_the_seat_and_a_paid_one_keeps_it() {
|
|
|
672
672
|
|
|
673
673
|
let (_, body) = t.register("declined").await;
|
|
674
674
|
let first = body["registration_id"].as_str().unwrap().to_string();
|
|
675
|
-
let response = web::registration_complete_route(&first, r#"{"outcome":"failed"}"#, &t.client, &t.wasm, &t.config, &NeverInvoker, &crate::
|
|
675
|
+
let response = web::registration_complete_route(&first, r#"{"outcome":"failed"}"#, &t.client, &t.wasm, &t.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
|
|
676
676
|
assert_eq!(response["statusCode"].as_u64(), Some(200));
|
|
677
677
|
assert_eq!(t.payment_status(&first).await, "failed");
|
|
678
678
|
|
|
@@ -714,7 +714,7 @@ async fn archiving_a_paid_registration_frees_its_seat_and_restoring_it_takes_the
|
|
|
714
714
|
assert_eq!(registration_status(&t, &reference).await, "archived");
|
|
715
715
|
assert_eq!(t.payment_status(&reference).await, "succeeded", "archiving never touches the Payment");
|
|
716
716
|
let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
|
|
717
|
-
assert_eq!(web::seats_left(&read, "CheckoutFixture", &crate::
|
|
717
|
+
assert_eq!(web::seats_left(&read, "CheckoutFixture", &crate::commerce_ir::fixture_payments(), "archive-me"), Some(1));
|
|
718
718
|
|
|
719
719
|
let (status, body) = t.register("archive-me").await;
|
|
720
720
|
assert_eq!(status, 200, "the archived registration's seat is free again: {body}");
|
|
@@ -728,11 +728,11 @@ async fn archiving_a_paid_registration_frees_its_seat_and_restoring_it_takes_the
|
|
|
728
728
|
assert_eq!(registration_status(&t, &reference).await, "active");
|
|
729
729
|
let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
|
|
730
730
|
assert_eq!(
|
|
731
|
-
web::seats_taken(&read, "CheckoutFixture", &crate::
|
|
731
|
+
web::seats_taken(&read, "CheckoutFixture", &crate::commerce_ir::fixture_payments(), "archive-me"),
|
|
732
732
|
2,
|
|
733
733
|
"a restored registration counts again, even past capacity: the site refuses a restore with no seat"
|
|
734
734
|
);
|
|
735
|
-
assert_eq!(web::seats_left(&read, "CheckoutFixture", &crate::
|
|
735
|
+
assert_eq!(web::seats_left(&read, "CheckoutFixture", &crate::commerce_ir::fixture_payments(), "archive-me"), Some(0));
|
|
736
736
|
}
|
|
737
737
|
|
|
738
738
|
#[tokio::test]
|
|
@@ -876,7 +876,7 @@ async fn an_event_verified_only_by_the_public_mock_secret_is_refused_for_a_real_
|
|
|
876
876
|
let payload = event.to_string();
|
|
877
877
|
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
|
|
878
878
|
let header = sign(web::MOCK_STRIPE_WEBHOOK_SECRET, now, &payload);
|
|
879
|
-
let response = web::webhook_route(&payload, &header, &t.platform, &t.client, &t.wasm, &t.config, &NeverInvoker, &crate::
|
|
879
|
+
let response = web::webhook_route(&payload, &header, &t.platform, &t.client, &t.wasm, &t.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
|
|
880
880
|
assert_eq!(response["statusCode"], 500, "{event}");
|
|
881
881
|
assert_eq!(t.payment_status(&reference).await, "pending", "a forged completion must not settle a real payment");
|
|
882
882
|
}
|