hecks 3.13.0 → 3.14.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/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- data/lib/hecks/bluebook/named_pattern.rb +33 -0
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
- data/lib/hecks/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/hecks/codebase.bluebook +37 -37
- data/lib/hecks/hecks/custodian.bluebook +14 -14
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/rehearsal.bluebook +3 -3
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- data/lib/hecks/version.rb +1 -1
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → 1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal} +0 -0
- data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal} +0 -0
- data/prebuilt/verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json +1 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/payments/roles.rs +159 -0
- data/rust/host/src/payments/tests.rs +25 -0
- data/rust/host/src/payments.rs +20 -11
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +23 -0
- data/rust/parser/src/keywords.rs +6 -0
- data/rust/parser/src/parse/mod.rs +22 -1
- metadata +7 -4
- data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.14.0
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//! Which signed-in roles may manage a site's payments: a per-site setting, read once at boot.
|
|
2
|
+
//!
|
|
3
|
+
//! `HECKS_PAYMENTS_MANAGER_ROLES` is a comma-separated list of exact role names. Unset means
|
|
4
|
+
//! `Owner`, which is what the host did before the setting existed. A set value must be a
|
|
5
|
+
//! plausible list or the boot is refused; a refusal names the variable and the kind of mistake,
|
|
6
|
+
//! never the value. A caller manages payments when their role is in the list, compared exactly:
|
|
7
|
+
//! no case folding, no hierarchy, no wildcard.
|
|
8
|
+
|
|
9
|
+
use std::sync::OnceLock;
|
|
10
|
+
|
|
11
|
+
/// The variable that names the roles allowed to manage payments.
|
|
12
|
+
pub const VARIABLE: &str = "HECKS_PAYMENTS_MANAGER_ROLES";
|
|
13
|
+
/// The role that manages payments when the variable is unset.
|
|
14
|
+
pub const DEFAULT_ROLE: &str = "Owner";
|
|
15
|
+
/// The most roles one list may name.
|
|
16
|
+
pub const MAX_ENTRIES: usize = 8;
|
|
17
|
+
/// The longest role name, after trimming.
|
|
18
|
+
pub const MAX_ENTRY_LENGTH: usize = 32;
|
|
19
|
+
|
|
20
|
+
/// The roles that manage payments when nothing is configured.
|
|
21
|
+
pub fn default_roles() -> Vec<String> {
|
|
22
|
+
vec![DEFAULT_ROLE.to_string()]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Parses the variable's value; `None` (unset) gives the default list.
|
|
26
|
+
pub fn parse(raw: Option<&str>) -> Result<Vec<String>, String> {
|
|
27
|
+
let Some(raw) = raw else { return Ok(default_roles()) };
|
|
28
|
+
let refuse = |why: &str| Err(format!("{VARIABLE} is invalid: {why}"));
|
|
29
|
+
let entries: Vec<&str> = raw.split(',').map(str::trim).collect();
|
|
30
|
+
if entries.len() > MAX_ENTRIES {
|
|
31
|
+
return refuse(&format!("it names more than {MAX_ENTRIES} roles"));
|
|
32
|
+
}
|
|
33
|
+
for (index, entry) in entries.iter().enumerate() {
|
|
34
|
+
let position = index + 1;
|
|
35
|
+
if entry.is_empty() {
|
|
36
|
+
return refuse(&format!("entry {position} is empty; name at least one role and no blank entries"));
|
|
37
|
+
}
|
|
38
|
+
if entry.len() > MAX_ENTRY_LENGTH {
|
|
39
|
+
return refuse(&format!("entry {position} is longer than {MAX_ENTRY_LENGTH} characters"));
|
|
40
|
+
}
|
|
41
|
+
let starts_well = entry.starts_with(|c: char| c.is_ascii_alphanumeric());
|
|
42
|
+
if !starts_well || !entry.chars().all(|c| c.is_ascii_alphanumeric() || c == ' ' || c == '-') {
|
|
43
|
+
return refuse(&format!("entry {position} is not a role name: letters, digits, spaces and hyphens only, starting with a letter or digit"));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
Ok(entries.into_iter().map(String::from).collect())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Reads the setting into `cell` the first time and never again, whatever the environment
|
|
50
|
+
/// does afterwards. `read` stands for the environment lookup.
|
|
51
|
+
pub fn read_once(cell: &OnceLock<Result<Vec<String>, String>>, read: impl FnOnce() -> Option<String>) -> &Result<Vec<String>, String> {
|
|
52
|
+
cell.get_or_init(|| parse(read().as_deref()))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// This process's setting, read from the environment on first use (the boot check is the first).
|
|
56
|
+
pub fn configured() -> &'static Result<Vec<String>, String> {
|
|
57
|
+
static CELL: OnceLock<Result<Vec<String>, String>> = OnceLock::new();
|
|
58
|
+
read_once(&CELL, || std::env::var(VARIABLE).ok())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Refuses the boot when this process's setting is invalid.
|
|
62
|
+
pub fn check_boot() -> Result<(), String> {
|
|
63
|
+
configured().as_ref().map(|_| ()).map_err(Clone::clone)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// The roles for a request. An invalid setting never gets here (the boot was refused); should it
|
|
67
|
+
/// ever, only the default role manages, so a mistake narrows access instead of widening it.
|
|
68
|
+
pub fn manager_roles() -> Vec<String> {
|
|
69
|
+
configured().clone().unwrap_or_else(|_| default_roles())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Whether `role` is one of `roles`, by exact string match.
|
|
73
|
+
pub fn manages(roles: &[String], role: &str) -> bool {
|
|
74
|
+
roles.iter().any(|allowed| allowed == role)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[cfg(test)]
|
|
78
|
+
mod tests {
|
|
79
|
+
use super::*;
|
|
80
|
+
|
|
81
|
+
fn roles(raw: Option<&str>) -> Vec<String> {
|
|
82
|
+
parse(raw).unwrap()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn unset_means_owner_only() {
|
|
87
|
+
let list = roles(None);
|
|
88
|
+
assert_eq!(list, ["Owner"]);
|
|
89
|
+
assert!(manages(&list, "Owner"));
|
|
90
|
+
assert!(!manages(&list, "Admin"));
|
|
91
|
+
assert!(!manages(&list, "Member"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn a_configured_list_admits_exactly_its_roles() {
|
|
96
|
+
let list = roles(Some("Owner,Admin"));
|
|
97
|
+
assert!(manages(&list, "Owner") && manages(&list, "Admin"));
|
|
98
|
+
assert!(!manages(&list, "Editor"));
|
|
99
|
+
assert!(!manages(&list, ""));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[test]
|
|
103
|
+
fn whitespace_is_trimmed_and_case_is_exact() {
|
|
104
|
+
let list = roles(Some(" Owner , Account Admin "));
|
|
105
|
+
assert_eq!(list, ["Owner", "Account Admin"]);
|
|
106
|
+
let lower = roles(Some("admin"));
|
|
107
|
+
assert!(manages(&lower, "admin"));
|
|
108
|
+
assert!(!manages(&lower, "Admin"), "no case folding");
|
|
109
|
+
assert!(!manages(&roles(Some("Admin")), "admin"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn no_wildcard_and_no_hierarchy() {
|
|
114
|
+
assert!(parse(Some("*")).is_err());
|
|
115
|
+
assert!(!manages(&roles(Some("Admin")), "Owner"), "naming Admin does not admit Owner");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn an_invalid_setting_is_refused_naming_the_variable() {
|
|
120
|
+
let too_many = ["Role"; MAX_ENTRIES + 1].join(",");
|
|
121
|
+
let too_long = "A".repeat(MAX_ENTRY_LENGTH + 1);
|
|
122
|
+
for bad in ["", " ", ",", "Owner,", ",Owner", "Owner,,Admin", "Own/er", "Owner\nAdmin", "Own\u{7}er", "Admin;Owner", "-Admin", "Ad_min", "Régisseur", too_many.as_str(), too_long.as_str()] {
|
|
123
|
+
let message = parse(Some(bad)).expect_err(bad);
|
|
124
|
+
assert!(message.starts_with(VARIABLE), "{message}");
|
|
125
|
+
}
|
|
126
|
+
assert!(parse(Some(&"A".repeat(MAX_ENTRY_LENGTH))).is_ok());
|
|
127
|
+
assert!(parse(Some(&["Role"; MAX_ENTRIES].join(","))).is_ok());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn a_refusal_never_echoes_the_value() {
|
|
132
|
+
let message = parse(Some("sk_live_SECRET/value")).unwrap_err();
|
|
133
|
+
assert!(!message.contains("sk_live"), "{message}");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn the_setting_is_read_once() {
|
|
138
|
+
let cell = OnceLock::new();
|
|
139
|
+
let reads = std::cell::Cell::new(0);
|
|
140
|
+
let first = read_once(&cell, || {
|
|
141
|
+
reads.set(reads.get() + 1);
|
|
142
|
+
Some("Owner,Admin".to_string())
|
|
143
|
+
});
|
|
144
|
+
assert_eq!(first.as_ref().unwrap(), &["Owner", "Admin"]);
|
|
145
|
+
let second = read_once(&cell, || {
|
|
146
|
+
reads.set(reads.get() + 1);
|
|
147
|
+
Some("Editor".to_string())
|
|
148
|
+
});
|
|
149
|
+
assert_eq!(second.as_ref().unwrap(), &["Owner", "Admin"]);
|
|
150
|
+
assert_eq!(reads.get(), 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn an_invalid_value_stays_refused_once_read() {
|
|
155
|
+
let cell = OnceLock::new();
|
|
156
|
+
assert!(read_once(&cell, || Some("a/b".to_string())).is_err());
|
|
157
|
+
assert!(read_once(&cell, || None).is_err());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -197,6 +197,7 @@ async fn tenant(name: &str) -> Tenant {
|
|
|
197
197
|
direct_live_publishable_key: String::new(),
|
|
198
198
|
webhook_secret: Some(WEBHOOK_SECRET.to_string()),
|
|
199
199
|
operators: vec![OPERATOR.to_string()],
|
|
200
|
+
manager_roles: roles::default_roles(),
|
|
200
201
|
};
|
|
201
202
|
Tenant {
|
|
202
203
|
client,
|
|
@@ -412,6 +413,30 @@ async fn only_an_owner_may_connect_or_disconnect_and_only_the_operator_may_enabl
|
|
|
412
413
|
assert_eq!(t.status().await, "enabled");
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
#[tokio::test]
|
|
417
|
+
async fn a_site_that_lists_admin_as_a_payments_manager_lets_an_admin_manage() {
|
|
418
|
+
let mut t = tenant("hecks_pay_test_manager_roles").await;
|
|
419
|
+
t.platform.manager_roles = vec!["Owner".to_string(), "Admin".to_string()];
|
|
420
|
+
|
|
421
|
+
let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(ADMIN)).await;
|
|
422
|
+
assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
|
|
423
|
+
t.connect().await;
|
|
424
|
+
assert_eq!(t.status().await, "connected");
|
|
425
|
+
let (status, _) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(ADMIN)).await;
|
|
426
|
+
assert_eq!(status, 200);
|
|
427
|
+
assert_eq!(t.status().await, "disconnected");
|
|
428
|
+
|
|
429
|
+
// Enabling stays with the operator, and the Owner still manages.
|
|
430
|
+
let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
|
|
431
|
+
assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
|
|
432
|
+
// Someone signed out, or not listed, is refused with today's message.
|
|
433
|
+
let (status, _) = t.call("GET", "/payments/connection", json!({}), None).await;
|
|
434
|
+
assert_eq!(status, 401);
|
|
435
|
+
t.platform.manager_roles = vec!["Editor".to_string()];
|
|
436
|
+
let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
|
|
437
|
+
assert_eq!((status, body["error"].as_str()), (403, Some("only an Owner can manage payments")));
|
|
438
|
+
}
|
|
439
|
+
|
|
415
440
|
#[tokio::test]
|
|
416
441
|
async fn an_admin_granted_owner_passes_the_payments_gate_and_keeps_the_admin_gate() {
|
|
417
442
|
let t = tenant("hecks_pay_test_owner_bootstrap").await;
|
data/rust/host/src/payments.rs
CHANGED
|
@@ -15,6 +15,8 @@ use tokio::sync::Mutex;
|
|
|
15
15
|
use tokio_postgres::Client;
|
|
16
16
|
|
|
17
17
|
mod keystore;
|
|
18
|
+
/// Which member roles may manage payments (`HECKS_PAYMENTS_MANAGER_ROLES`).
|
|
19
|
+
pub mod roles;
|
|
18
20
|
use keystore::{KeyStore, StoredDocument, StoredKeys};
|
|
19
21
|
|
|
20
22
|
const CONNECTION_SLUG: &str = "payments";
|
|
@@ -63,11 +65,16 @@ pub struct PlatformConfig {
|
|
|
63
65
|
pub webhook_secret: Option<String>,
|
|
64
66
|
/// Lower-cased emails allowed to enable or disable real payments.
|
|
65
67
|
pub operators: Vec<String>,
|
|
68
|
+
/// The exact role names that may manage payments (`HECKS_PAYMENTS_MANAGER_ROLES`,
|
|
69
|
+
/// default `Owner`), read once at boot; see `roles`.
|
|
70
|
+
pub manager_roles: Vec<String>,
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
/// Refuses the boot
|
|
69
|
-
/// names no secret; off AWS, or with
|
|
73
|
+
/// Refuses the boot when `HECKS_PAYMENTS_MANAGER_ROLES` is invalid, and on AWS when
|
|
74
|
+
/// checkout is enabled but `PAYMENTS_ACCOUNT_SECRET_ID` names no secret; off AWS, or with
|
|
75
|
+
/// checkout off, the host boots as usual.
|
|
70
76
|
pub fn check_boot(checkout_enabled: bool) -> Result<(), String> {
|
|
77
|
+
roles::check_boot()?;
|
|
71
78
|
keystore::check_boot(checkout_enabled)
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -102,6 +109,7 @@ impl PlatformConfig {
|
|
|
102
109
|
.map(|email| email.trim().to_lowercase())
|
|
103
110
|
.filter(|email| !email.is_empty())
|
|
104
111
|
.collect(),
|
|
112
|
+
manager_roles: roles::manager_roles(),
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -276,7 +284,7 @@ fn refusal_message(refusal: &Value) -> String {
|
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
struct Caller {
|
|
279
|
-
|
|
287
|
+
manager: bool,
|
|
280
288
|
operator: bool,
|
|
281
289
|
}
|
|
282
290
|
|
|
@@ -288,14 +296,14 @@ async fn caller(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &s
|
|
|
288
296
|
return Err(not_logged_in());
|
|
289
297
|
};
|
|
290
298
|
match auth::active_role(client, domain_ir, &email).await {
|
|
291
|
-
Ok(Some(role)) => Ok(Caller {
|
|
299
|
+
Ok(Some(role)) => Ok(Caller { manager: roles::manages(&platform.manager_roles, &role), operator: platform.is_operator(&email) }),
|
|
292
300
|
Ok(None) => Err(not_logged_in()),
|
|
293
301
|
Err(e) => Err(json_error(500, &format!("members lookup failed: {e}"))),
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
|
|
297
|
-
fn
|
|
298
|
-
if caller.
|
|
305
|
+
fn require_manager(caller: &Caller) -> Result<(), Value> {
|
|
306
|
+
if caller.manager { Ok(()) } else { Err(json_error(403, "only an Owner can manage payments")) }
|
|
299
307
|
}
|
|
300
308
|
|
|
301
309
|
fn require_operator(caller: &Caller) -> Result<(), Value> {
|
|
@@ -322,7 +330,7 @@ fn connection_json(connection: Option<&Connection>, platform: &PlatformConfig, c
|
|
|
322
330
|
"direct_modes": platform.direct_modes(),
|
|
323
331
|
"direct": connection.is_some_and(Connection::is_direct),
|
|
324
332
|
"can_save_keys": platform.can_save_keys(),
|
|
325
|
-
"can_manage": caller.
|
|
333
|
+
"can_manage": caller.manager,
|
|
326
334
|
"can_enable": caller.operator,
|
|
327
335
|
})
|
|
328
336
|
}
|
|
@@ -375,9 +383,9 @@ pub async fn route(
|
|
|
375
383
|
})
|
|
376
384
|
}
|
|
377
385
|
|
|
378
|
-
// GET: readable by
|
|
386
|
+
// GET: readable by a payments manager or the operator, nobody else.
|
|
379
387
|
async fn show(caller: &Caller, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig) -> Value {
|
|
380
|
-
if !caller.
|
|
388
|
+
if !caller.manager && !caller.operator {
|
|
381
389
|
return json_error(403, "not permitted");
|
|
382
390
|
}
|
|
383
391
|
connection_response(caller, platform, client, wasm_path, config).await
|
|
@@ -429,7 +437,7 @@ async fn record_connection(
|
|
|
429
437
|
// name), and the key itself is never stored or logged.
|
|
430
438
|
#[allow(clippy::too_many_arguments)]
|
|
431
439
|
async fn direct_route(caller: &Caller, raw_body: &str, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
|
|
432
|
-
if let Err(response) =
|
|
440
|
+
if let Err(response) = require_manager(caller) {
|
|
433
441
|
return response;
|
|
434
442
|
}
|
|
435
443
|
let body = match parse_body(raw_body) {
|
|
@@ -462,7 +470,7 @@ async fn direct_route(caller: &Caller, raw_body: &str, platform: &PlatformConfig
|
|
|
462
470
|
// guests have paid through this account. Disconnecting the business's own account
|
|
463
471
|
// also removes its webhook and deletes the saved keys.
|
|
464
472
|
async fn disconnect_route(caller: &Caller, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
|
|
465
|
-
if let Err(response) =
|
|
473
|
+
if let Err(response) = require_manager(caller) {
|
|
466
474
|
return response;
|
|
467
475
|
}
|
|
468
476
|
let existing = match current_connection(client, wasm_path, config).await {
|
|
@@ -762,6 +770,7 @@ pub(crate) fn test_platform() -> PlatformConfig {
|
|
|
762
770
|
direct_live_publishable_key: String::new(),
|
|
763
771
|
webhook_secret: None,
|
|
764
772
|
operators: Vec::new(),
|
|
773
|
+
manager_roles: roles::default_roles(),
|
|
765
774
|
}
|
|
766
775
|
}
|
|
767
776
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//! The patterns a bluebook names instead of spelling: `matches: :snake_name` stands for exactly
|
|
2
|
+
//! its regex. Mirrors `Hecks::Bluebook::NamedPattern`; spec/named_pattern_spec.rb holds the two
|
|
3
|
+
//! tables to each other.
|
|
4
|
+
|
|
5
|
+
/// Name => the regex source it stands for.
|
|
6
|
+
const TABLE: &[(&str, &str)] = &[
|
|
7
|
+
("present", r"[^ \t\n\r]"),
|
|
8
|
+
("not_empty", r"[^\t\n\r]"),
|
|
9
|
+
("no_whitespace", r"^[^ \t\n\r]+$"),
|
|
10
|
+
("snake_name", r"^[a-z][a-z0-9_]*$"),
|
|
11
|
+
("git_sha", r"^[0-9a-fA-F]{7,40}$"),
|
|
12
|
+
("digits", r"^[0-9]+$"),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/// The regex source `name` stands for, or `None` when the language gives no such name.
|
|
16
|
+
pub fn source(name: &str) -> Option<&'static str> {
|
|
17
|
+
TABLE.iter().find(|(given, _)| *given == name).map(|(_, source)| *source)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Every name the language gives, in table order.
|
|
21
|
+
pub fn names() -> Vec<&'static str> {
|
|
22
|
+
TABLE.iter().map(|(name, _)| *name).collect()
|
|
23
|
+
}
|
data/rust/parser/src/keywords.rs
CHANGED
|
@@ -256,6 +256,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
256
256
|
ArgumentRow { keyword: "attribute", context: "Aggregate", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
257
257
|
ArgumentRow { keyword: "attribute", context: "Aggregate", at: "", named: "optional", kind: "flag", required: "false", fills: "optional", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
258
258
|
ArgumentRow { keyword: "attribute", context: "Aggregate", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
259
|
+
ArgumentRow { keyword: "attribute", context: "Aggregate", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
259
260
|
ArgumentRow { keyword: "attribute", context: "Aggregate", at: "", named: "admits", kind: "text", required: "false", fills: "admits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
260
261
|
ArgumentRow { keyword: "attribute", context: "Aggregate", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
261
262
|
ArgumentRow { keyword: "invariant", context: "Aggregate", at: "1", named: "", kind: "text", required: "true", fills: "description", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
@@ -356,6 +357,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
356
357
|
ArgumentRow { keyword: "attribute", context: "Command", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
357
358
|
ArgumentRow { keyword: "attribute", context: "Command", at: "", named: "optional", kind: "flag", required: "false", fills: "optional", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
358
359
|
ArgumentRow { keyword: "attribute", context: "Command", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
360
|
+
ArgumentRow { keyword: "attribute", context: "Command", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
359
361
|
ArgumentRow { keyword: "attribute", context: "Command", at: "", named: "admits", kind: "text", required: "false", fills: "admits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
360
362
|
ArgumentRow { keyword: "attribute", context: "Command", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
361
363
|
ArgumentRow { keyword: "corrects", context: "Command", at: "1", named: "", kind: "text", required: "true", fills: "target", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
@@ -380,6 +382,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
380
382
|
ArgumentRow { keyword: "attribute", context: "Entity", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
381
383
|
ArgumentRow { keyword: "attribute", context: "Entity", at: "", named: "optional", kind: "flag", required: "false", fills: "optional", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
382
384
|
ArgumentRow { keyword: "attribute", context: "Entity", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
385
|
+
ArgumentRow { keyword: "attribute", context: "Entity", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
383
386
|
ArgumentRow { keyword: "attribute", context: "Entity", at: "", named: "admits", kind: "text", required: "false", fills: "admits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
384
387
|
ArgumentRow { keyword: "attribute", context: "Entity", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
385
388
|
ArgumentRow { keyword: "reference_to", context: "Entity", at: "1", named: "", kind: "constant", required: "true", fills: "type", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
@@ -446,6 +449,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
446
449
|
ArgumentRow { keyword: "attribute", context: "Query", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
447
450
|
ArgumentRow { keyword: "attribute", context: "Query", at: "", named: "optional", kind: "flag", required: "false", fills: "optional", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
448
451
|
ArgumentRow { keyword: "attribute", context: "Query", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
452
|
+
ArgumentRow { keyword: "attribute", context: "Query", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
449
453
|
ArgumentRow { keyword: "attribute", context: "Query", at: "", named: "admits", kind: "text", required: "false", fills: "admits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
450
454
|
ArgumentRow { keyword: "attribute", context: "Query", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
451
455
|
ArgumentRow { keyword: "reference_to", context: "Query", at: "1", named: "", kind: "constant", required: "true", fills: "type", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
@@ -464,6 +468,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
464
468
|
ArgumentRow { keyword: "attribute", context: "ValueObject", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
465
469
|
ArgumentRow { keyword: "attribute", context: "ValueObject", at: "", named: "optional", kind: "flag", required: "false", fills: "optional", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
466
470
|
ArgumentRow { keyword: "attribute", context: "ValueObject", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
471
|
+
ArgumentRow { keyword: "attribute", context: "ValueObject", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
467
472
|
ArgumentRow { keyword: "attribute", context: "ValueObject", at: "", named: "admits", kind: "text", required: "false", fills: "admits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
468
473
|
ArgumentRow { keyword: "attribute", context: "ValueObject", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
469
474
|
ArgumentRow { keyword: "invariant", context: "ValueObject", at: "1", named: "", kind: "text", required: "true", fills: "description", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
@@ -500,6 +505,7 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
500
505
|
ArgumentRow { keyword: "attribute", context: "PortOperation", at: "", named: "default", kind: "literal", required: "false", fills: "default", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
501
506
|
ArgumentRow { keyword: "attribute", context: "PortOperation", at: "", named: "optional", kind: "flag", required: "false", fills: "optional", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
502
507
|
ArgumentRow { keyword: "attribute", context: "PortOperation", at: "", named: "pattern", kind: "text", required: "false", fills: "pattern", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
508
|
+
ArgumentRow { keyword: "attribute", context: "PortOperation", at: "", named: "matches", kind: "symbol", required: "false", fills: "matches", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
503
509
|
ArgumentRow { keyword: "attribute", context: "PortOperation", at: "", named: "admits", kind: "text", required: "false", fills: "admits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
504
510
|
ArgumentRow { keyword: "attribute", context: "PortOperation", at: "", named: "one_of", kind: "list", required: "false", fills: "one_of", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
505
511
|
ArgumentRow { keyword: "emits", context: "PortOperation", at: "1", named: "", kind: "text", required: "true", fills: "emits", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
@@ -1134,7 +1134,28 @@ pub(crate) fn build_attribute(
|
|
|
1134
1134
|
let (type_name, list, closed_set) = resolve_type_expression(file, line, word, &name, text)?;
|
|
1135
1135
|
let default = named_raw(args, "default").map(ruby_value::read);
|
|
1136
1136
|
let optional = named_flag(args, "optional");
|
|
1137
|
-
|
|
1137
|
+
// `matches: :name` is the pattern the language gives that name.
|
|
1138
|
+
let pattern = match named_symbol(args, "matches") {
|
|
1139
|
+
Some(given) => {
|
|
1140
|
+
if named_text(args, "pattern").is_some() {
|
|
1141
|
+
return Err(Diagnostic::new(file, line, "matches: and pattern: both state the pattern".to_string()));
|
|
1142
|
+
}
|
|
1143
|
+
match crate::build::named_pattern::source(&given) {
|
|
1144
|
+
Some(source) => Some(source.to_string()),
|
|
1145
|
+
None => {
|
|
1146
|
+
return Err(Diagnostic::new(
|
|
1147
|
+
file,
|
|
1148
|
+
line,
|
|
1149
|
+
format!(
|
|
1150
|
+
"matches: {given:?} is not a named pattern — the names are {}",
|
|
1151
|
+
crate::build::named_pattern::names().join(", ")
|
|
1152
|
+
),
|
|
1153
|
+
))
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
None => named_text(args, "pattern"),
|
|
1158
|
+
};
|
|
1138
1159
|
let admits = named_text(args, "admits");
|
|
1139
1160
|
// Fail closed at declaration time on a pattern outside the shared subset, as Ruby does.
|
|
1140
1161
|
if let Some(pat) = &pattern {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
@@ -322,6 +322,7 @@ files:
|
|
|
322
322
|
- lib/hecks/bluebook/model_check/reference_checks.rb
|
|
323
323
|
- lib/hecks/bluebook/model_check/saga_checks.rb
|
|
324
324
|
- lib/hecks/bluebook/model_check/verbs.rb
|
|
325
|
+
- lib/hecks/bluebook/named_pattern.rb
|
|
325
326
|
- lib/hecks/bluebook/pattern_subset.rb
|
|
326
327
|
- lib/hecks/bluebook/pattern_subset/refusals.rb
|
|
327
328
|
- lib/hecks/bluebook/pattern_subset/scan.rb
|
|
@@ -1636,9 +1637,9 @@ files:
|
|
|
1636
1637
|
- lib/rubocop/cop/hecks/symbol_string_key_mix.rb
|
|
1637
1638
|
- lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb
|
|
1638
1639
|
- lib/rubocop/cop/hecks/untyped_raise.rb
|
|
1639
|
-
- prebuilt/
|
|
1640
|
-
- prebuilt/
|
|
1641
|
-
- prebuilt/verdicts-
|
|
1640
|
+
- prebuilt/1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal
|
|
1641
|
+
- prebuilt/a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal
|
|
1642
|
+
- prebuilt/verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json
|
|
1642
1643
|
- qa/settings.yml
|
|
1643
1644
|
- rust/.cargo/config.toml
|
|
1644
1645
|
- rust/.cargo/mutants.toml
|
|
@@ -1734,6 +1735,7 @@ files:
|
|
|
1734
1735
|
- rust/host/src/needs.rs
|
|
1735
1736
|
- rust/host/src/payments.rs
|
|
1736
1737
|
- rust/host/src/payments/keystore.rs
|
|
1738
|
+
- rust/host/src/payments/roles.rs
|
|
1737
1739
|
- rust/host/src/payments/tests.rs
|
|
1738
1740
|
- rust/host/src/platform_signin/attack_tests.rs
|
|
1739
1741
|
- rust/host/src/platform_signin/config.rs
|
|
@@ -1804,6 +1806,7 @@ files:
|
|
|
1804
1806
|
- rust/parser/src/build/closed_sets.rs
|
|
1805
1807
|
- rust/parser/src/build/identity.rs
|
|
1806
1808
|
- rust/parser/src/build/mod.rs
|
|
1809
|
+
- rust/parser/src/build/named_pattern.rs
|
|
1807
1810
|
- rust/parser/src/build/naming.rs
|
|
1808
1811
|
- rust/parser/src/build/pattern_subset.rs
|
|
1809
1812
|
- rust/parser/src/build/query_derive.rs
|