Skip to content

Commit e4c87fa

Browse files
authored
Support places in Attribute scopes (#72)
We had the notion of both roles and places in Attributes in Technique v0, but so far in v1 we'd only implemented role Attributes with the `@role` notation. This branch adds `^place`, using the caret we're now using as the marker for place Attributes.
2 parents ca3b719 + 7704f9c commit e4c87fa

File tree

5 files changed

+141
-78
lines changed

5 files changed

+141
-78
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "technique"
3-
version = "0.3.5"
3+
version = "0.3.6"
44
edition = "2021"
55
description = "A domain specific language for procedures."
66
authors = [ "Andrew Cowie" ]

examples/golden/ManyAttributes.t

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
% technique v1
2+
3+
making_coffee :
4+
5+
These are the steps.
6+
7+
1. First
8+
@chef
9+
- First sub task
10+
- Second sub task
11+
2. Second
12+
@chef + @barista
13+
- Third sub task
14+
- Fourth sub task
15+
3. Third
16+
^kitchen
17+
- Fifth sub task
18+
- Sixth sub task
19+
4. Four
20+
^kitchen + ^bathroom
21+
- Seventh sub task
22+
- Eighth sub task
23+
5. Five
24+
@chef + ^bathroom
25+
- Ninth sub task
26+
- Tenth sub task
27+
6. Six
28+
^kitchen + @barista
29+
- Ninth sub task
30+
- Tenth sub task

src/formatting/formatter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ impl<'i> Formatter<'i> {
866866
self.add_fragment_reference(Syntax::Attribute, name.0);
867867
}
868868
Attribute::Place(name) => {
869-
self.add_fragment_reference(Syntax::Attribute, "#");
869+
self.add_fragment_reference(Syntax::Attribute, "^");
870870
self.add_fragment_reference(Syntax::Attribute, name.0);
871871
}
872872
}

src/parsing/parser.rs

Lines changed: 108 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ impl<'i> Parser<'i> {
765765
} else if is_code_block(content) {
766766
let expression = outer.read_code_block()?;
767767
elements.push(Element::CodeBlock(expression));
768-
} else if is_role_assignment(content) {
768+
} else if is_attribute_assignment(content) {
769769
let attribute_block = outer.read_attribute_scope()?;
770770
elements.push(Element::Steps(vec![attribute_block]));
771771
} else if is_step(content) {
@@ -796,14 +796,14 @@ impl<'i> Parser<'i> {
796796
&& !is_procedure_title(line)
797797
&& !is_code_block(line)
798798
&& !malformed_step_pattern(line)
799-
&& !is_role_assignment(line)
799+
&& !is_attribute_assignment(line)
800800
},
801801
|line| {
802802
is_step(line)
803803
|| is_procedure_title(line)
804804
|| is_code_block(line)
805805
|| malformed_step_pattern(line)
806-
|| is_role_assignment(line)
806+
|| is_attribute_assignment(line)
807807
},
808808
|inner| {
809809
let content = inner.source;
@@ -1672,7 +1672,7 @@ impl<'i> Parser<'i> {
16721672
|| is_substep_dependent(line)
16731673
|| is_substep_parallel(line)
16741674
|| is_subsubstep_dependent(line)
1675-
|| is_role_assignment(line)
1675+
|| is_attribute_assignment(line)
16761676
|| is_enum_response(line)
16771677
|| malformed_step_pattern(line)
16781678
|| malformed_response_pattern(line)
@@ -1927,32 +1927,43 @@ impl<'i> Parser<'i> {
19271927
self.offset += i;
19281928
}
19291929

1930-
/// Parse role assignments like @surgeon, @nurse, or @marketing + @sales
1931-
fn read_role_assignments(&mut self) -> Result<Vec<Attribute<'i>>, ParsingError<'i>> {
1930+
/// Parse attributes (roles and/or places) like @surgeon, ^kitchen, or @chef + ^bathroom
1931+
fn read_attributes(&mut self) -> Result<Vec<Attribute<'i>>, ParsingError<'i>> {
19321932
self.take_line(|inner| {
19331933
let mut attributes = Vec::new();
19341934

19351935
let line = inner.source;
19361936

1937-
// Handle multiple roles separated by +
1938-
let role_parts: Vec<&str> = line
1937+
// Handle multiple attributes separated by +
1938+
let parts: Vec<&str> = line
19391939
.split('+')
19401940
.collect();
19411941

1942-
for part in role_parts {
1943-
let re = regex!(r"^\s*@([a-z][a-z0-9_]*)\s*$");
1944-
let cap = re
1945-
.captures(part.trim_ascii())
1946-
.ok_or(ParsingError::InvalidStep(inner.offset))?;
1947-
1948-
let role_name = cap
1949-
.get(1)
1950-
.ok_or(ParsingError::Expected(inner.offset, "role name after @"))?
1951-
.as_str();
1952-
1953-
let identifier = validate_identifier(role_name)
1954-
.ok_or(ParsingError::InvalidIdentifier(inner.offset, role_name))?;
1955-
attributes.push(Attribute::Role(identifier));
1942+
for part in parts {
1943+
let trimmed = part.trim_ascii();
1944+
1945+
// Check if it's a role '@'
1946+
if let Some(captures) = regex!(r"^@([a-z][a-z0-9_]*)$").captures(trimmed) {
1947+
let role_name = captures
1948+
.get(1)
1949+
.ok_or(ParsingError::Expected(inner.offset, "role name after @"))?
1950+
.as_str();
1951+
let identifier = validate_identifier(role_name)
1952+
.ok_or(ParsingError::InvalidIdentifier(inner.offset, role_name))?;
1953+
attributes.push(Attribute::Role(identifier));
1954+
}
1955+
// Check if it's a place '^'
1956+
else if let Some(captures) = regex!(r"^\^([a-z][a-z0-9_]*)$").captures(trimmed) {
1957+
let place_name = captures
1958+
.get(1)
1959+
.ok_or(ParsingError::Expected(inner.offset, "place name after ^"))?
1960+
.as_str();
1961+
let identifier = validate_identifier(place_name)
1962+
.ok_or(ParsingError::InvalidIdentifier(inner.offset, place_name))?;
1963+
attributes.push(Attribute::Place(identifier));
1964+
} else {
1965+
return Err(ParsingError::InvalidStep(inner.offset));
1966+
}
19561967
}
19571968

19581969
Ok(attributes)
@@ -1972,7 +1983,7 @@ impl<'i> Parser<'i> {
19721983

19731984
let content = self.source;
19741985

1975-
if is_role_assignment(content) {
1986+
if is_attribute_assignment(content) {
19761987
let block = self.read_attribute_scope()?;
19771988
scopes.push(block);
19781989
} else if is_substep_dependent(content) {
@@ -2004,10 +2015,10 @@ impl<'i> Parser<'i> {
20042015
Ok(scopes)
20052016
}
20062017

2007-
/// Parse an attribute block (role assignment) with its subscopes
2018+
/// Parse an attribute block (role or place assignment) with its subscopes
20082019
fn read_attribute_scope(&mut self) -> Result<Scope<'i>, ParsingError<'i>> {
2009-
self.take_block_lines(is_role_assignment, is_role_assignment, |outer| {
2010-
let attributes = outer.read_role_assignments()?;
2020+
self.take_block_lines(is_attribute_assignment, is_attribute_assignment, |outer| {
2021+
let attributes = outer.read_attributes()?;
20112022
let subscopes = outer.read_scopes()?;
20122023

20132024
Ok(Scope::AttributeBlock {
@@ -2266,7 +2277,7 @@ fn is_procedure_body(content: &str) -> bool {
22662277
// Check for procedure body indicators. At the end, if it doesn't look like signature, it's body.
22672278
is_procedure_title(content)
22682279
|| is_step(content)
2269-
|| is_role_assignment(content)
2280+
|| is_attribute_assignment(content)
22702281
|| is_code_block(content)
22712282
|| is_enum_response(content)
22722283
|| (!is_signature_part(content))
@@ -2390,11 +2401,6 @@ fn is_subsubstep_dependent(content: &str) -> bool {
23902401
re.is_match(content)
23912402
}
23922403

2393-
fn is_role_assignment(content: &str) -> bool {
2394-
let re = regex!(r"^\s*@[a-z][a-z0-9_]*(\s*\+\s*@[a-z][a-z0-9_]*)*");
2395-
re.is_match(content)
2396-
}
2397-
23982404
fn is_enum_response(content: &str) -> bool {
23992405
let re = regex!(r"^\s*'.+?'");
24002406
re.is_match(content)
@@ -2427,6 +2433,12 @@ fn is_string_literal(content: &str) -> bool {
24272433
re.is_match(content)
24282434
}
24292435

2436+
fn is_attribute_assignment(input: &str) -> bool {
2437+
// Matches any combination of @ and ^ attributes separated by +
2438+
let re = regex!(r"^\s*[@^][a-z][a-z0-9_]*(\s*\+\s*[@^][a-z][a-z0-9_]*)*");
2439+
re.is_match(input)
2440+
}
2441+
24302442
#[cfg(test)]
24312443
mod check {
24322444
use super::*;
@@ -2955,11 +2967,16 @@ making_coffee(b, m) :
29552967
assert!(is_subsubstep_dependent("xi. Eleven"));
29562968
assert!(is_subsubstep_dependent("xxxix. Thirty-nine"));
29572969

2958-
// Test role assignments
2959-
assert!(is_role_assignment("@surgeon"));
2960-
assert!(is_role_assignment(" @nursing_team"));
2961-
assert!(!is_role_assignment("surgeon"));
2962-
assert!(!is_role_assignment("@123invalid"));
2970+
// Test attribute assignments
2971+
assert!(is_attribute_assignment("@surgeon"));
2972+
assert!(is_attribute_assignment(" @nursing_team"));
2973+
assert!(is_attribute_assignment("^kitchen"));
2974+
assert!(is_attribute_assignment(" ^garden "));
2975+
assert!(is_attribute_assignment("@chef + ^kitchen"));
2976+
assert!(is_attribute_assignment("^room1 + @barista"));
2977+
assert!(!is_attribute_assignment("surgeon"));
2978+
assert!(!is_attribute_assignment("@123invalid"));
2979+
assert!(!is_attribute_assignment("^InvalidPlace"));
29632980

29642981
// Test enum responses
29652982
assert!(is_enum_response("'Yes'"));
@@ -4126,68 +4143,84 @@ echo test
41264143
}
41274144

41284145
#[test]
4129-
fn reading_role_assignments() {
4146+
fn reading_attributes() {
41304147
let mut input = Parser::new();
41314148

4132-
// Test simple role assignment
4133-
input.initialize("@surgeon");
4134-
let result = input.read_role_assignments();
4135-
assert_eq!(result, Ok(vec![Attribute::Role(Identifier("surgeon"))]));
4149+
// Test simple role
4150+
input.initialize("@chef");
4151+
let result = input.read_attributes();
4152+
assert_eq!(result, Ok(vec![Attribute::Role(Identifier("chef"))]));
41364153

4137-
// Test role assignment with whitespace
4138-
input.initialize(" @nurse ");
4139-
let result = input.read_role_assignments();
4140-
assert_eq!(result, Ok(vec![Attribute::Role(Identifier("nurse"))]));
4154+
// Test simple place
4155+
input.initialize("^kitchen");
4156+
let result = input.read_attributes();
4157+
assert_eq!(result, Ok(vec![Attribute::Place(Identifier("kitchen"))]));
41414158

4142-
// Test role assignment with underscores
4143-
input.initialize("@nursing_team");
4144-
let result = input.read_role_assignments();
4159+
// Test multiple roles
4160+
input.initialize("@master_chef + @barista");
4161+
let result = input.read_attributes();
41454162
assert_eq!(
41464163
result,
4147-
Ok(vec![Attribute::Role(Identifier("nursing_team"))])
4164+
Ok(vec![
4165+
Attribute::Role(Identifier("master_chef")),
4166+
Attribute::Role(Identifier("barista"))
4167+
])
41484168
);
41494169

4150-
// Test role assignment with numbers
4151-
input.initialize("@team1");
4152-
let result = input.read_role_assignments();
4153-
assert_eq!(result, Ok(vec![Attribute::Role(Identifier("team1"))]));
4170+
// Test multiple places
4171+
input.initialize("^kitchen + ^bath_room");
4172+
let result = input.read_attributes();
4173+
assert_eq!(
4174+
result,
4175+
Ok(vec![
4176+
Attribute::Place(Identifier("kitchen")),
4177+
Attribute::Place(Identifier("bath_room"))
4178+
])
4179+
);
41544180

4155-
// Test multiple roles with +
4156-
input.initialize("@marketing + @sales");
4157-
let result = input.read_role_assignments();
4181+
// Test mixed roles and places
4182+
input.initialize("@chef + ^bathroom");
4183+
let result = input.read_attributes();
41584184
assert_eq!(
41594185
result,
41604186
Ok(vec![
4161-
Attribute::Role(Identifier("marketing")),
4162-
Attribute::Role(Identifier("sales"))
4187+
Attribute::Role(Identifier("chef")),
4188+
Attribute::Place(Identifier("bathroom"))
41634189
])
41644190
);
41654191

4166-
// Test multiple roles with + and extra whitespace
4167-
input.initialize("@operators + @users + @management");
4168-
let result = input.read_role_assignments();
4192+
// Test mixed places and roles
4193+
input.initialize("^kitchen + @barista");
4194+
let result = input.read_attributes();
41694195
assert_eq!(
41704196
result,
41714197
Ok(vec![
4172-
Attribute::Role(Identifier("operators")),
4173-
Attribute::Role(Identifier("users")),
4174-
Attribute::Role(Identifier("management"))
4198+
Attribute::Place(Identifier("kitchen")),
4199+
Attribute::Role(Identifier("barista"))
41754200
])
41764201
);
41774202

4178-
// Test invalid role assignment - uppercase
4179-
input.initialize("@Surgeon");
4180-
let result = input.read_role_assignments();
4181-
assert!(result.is_err());
4203+
// Test complex mixed attributes
4204+
input.initialize("@chef + ^kitchen + @barista + ^dining_room");
4205+
let result = input.read_attributes();
4206+
assert_eq!(
4207+
result,
4208+
Ok(vec![
4209+
Attribute::Role(Identifier("chef")),
4210+
Attribute::Place(Identifier("kitchen")),
4211+
Attribute::Role(Identifier("barista")),
4212+
Attribute::Place(Identifier("dining_room"))
4213+
])
4214+
);
41824215

4183-
// Test invalid role assignment - missing @
4184-
input.initialize("surgeon");
4185-
let result = input.read_role_assignments();
4216+
// Test invalid - uppercase
4217+
input.initialize("^Kitchen");
4218+
let result = input.read_attributes();
41864219
assert!(result.is_err());
41874220

4188-
// Test invalid role assignment - empty
4189-
input.initialize("@");
4190-
let result = input.read_role_assignments();
4221+
// Test invalid - no marker
4222+
input.initialize("kitchen");
4223+
let result = input.read_attributes();
41914224
assert!(result.is_err());
41924225
}
41934226

0 commit comments

Comments
 (0)