Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions crates/resource/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ pub use builder::{BuildError, ResourceBuilder, ResourceParts};
/// ## Requirements
///
/// 1. [`Capacity`] identifiers are unique within a resource.
/// 2. If and only if any of the resource's capacities have a bound, the
/// resource entity has at least one event (the "bounds event") which
/// declares the bounds of all capacities that are bounded.
/// 2. If and only if any capacity has a bound, the resource has at least one
/// event carrying its bounds record. The record declares all bounded
/// capacities.
/// 3. An entity can use some quantity of a resource's capacities if and
/// only if it is an FSM.
/// 4. The resource named by a usage or bounds is a declared resource.
/// 5. A usage claims only capacities declared by its resource.
/// 6. A usage record is used only as the data carried by an entity reference.
/// 6. A usage record is used only as data carried by an entity reference and
/// cannot be nested in a list within that data.
/// 7. A bounds record is used only by events of the resource it names.
/// 8. Bounds records may be optional, but cannot be used in a list.
///
/// ## Unit resources
///
Expand Down Expand Up @@ -154,6 +156,8 @@ struct BoundsRecord {
struct RecordRef {
record: Identifier,
on_entity_ref: bool,
in_list: bool,
in_reference_list: bool,
entity: Option<(Identifier, bool)>,
location: String,
}
Expand Down Expand Up @@ -233,12 +237,21 @@ impl Visitor for ResourceConstraint {
// Record roles are visited after entity fields, so references are
// resolved in `finish`.
Element::DataType(DataType::Record(record)) => {
let entity_ref_index = cursor.elements().iter().rposition(|element| {
matches!(element, Element::DataType(DataType::EntityRef { .. }))
});
self.record_refs.push(RecordRef {
record: record.clone(),
on_entity_ref: matches!(
cursor.previous(),
Some(Element::DataType(DataType::EntityRef { .. }))
),
on_entity_ref: entity_ref_index.is_some(),
in_list: cursor
.elements()
.iter()
.any(|element| matches!(element, Element::DataType(DataType::List(_)))),
in_reference_list: entity_ref_index.is_some_and(|index| {
cursor.elements()[index + 1..]
.iter()
.any(|element| matches!(element, Element::DataType(DataType::List(_))))
}),
entity: enclosing_entity(cursor).map(|entity| {
(
entity.name().clone(),
Expand Down Expand Up @@ -294,11 +307,19 @@ impl Visitor for ResourceConstraint {
for RecordRef {
record,
on_entity_ref,
in_list,
in_reference_list,
entity,
location,
} in &record_refs
{
if let Some(usage) = usage_records.get(record) {
if *in_reference_list {
errors.push(ResourceError::UsageInList {
location: location.clone(),
});
continue;
}
// Requirement 6: a usage is carried by an entity reference.
if !on_entity_ref {
errors.push(ResourceError::UsageNotOnReference {
Expand All @@ -324,6 +345,12 @@ impl Visitor for ResourceConstraint {
}),
}
} else if let Some(bounds) = bounds_records.get(record) {
if *in_list {
errors.push(ResourceError::BoundsInList {
location: location.clone(),
});
continue;
}
// Requirement 7: a bounds record belongs to its resource, so the
// entity referencing it must be that resource.
let on_resource = matches!(entity, Some((entity, _)) if entity == &bounds.resource);
Expand Down Expand Up @@ -486,6 +513,8 @@ pub enum ResourceError {
},
#[error("{location}: a usage record is used outside an entity reference")]
UsageNotOnReference { location: String },
#[error("{location}: a usage record cannot be nested in list-valued reference data")]
UsageInList { location: String },
#[error("entity \"{entity}\" uses resource \"{resource}\" but is not an FSM")]
NonFsmUser {
entity: Identifier,
Expand All @@ -496,6 +525,8 @@ pub enum ResourceError {
location: String,
resource: Identifier,
},
#[error("{location}: a bounds record cannot be used in a list")]
BoundsInList { location: String },
#[error(
"{location}: bounds declare \"{capacity}\", which resource \"{resource}\" does not bound"
)]
Expand Down
94 changes: 89 additions & 5 deletions crates/resource/tests/resource-constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,18 @@ fn bounds_record(name: &str, resource: &str, fields: &[&str]) -> Record {

/// Create a resource entity with a bounds event referencing `bounds` when given.
fn resource_entity(name: &str, capacities: &[(&str, &str, bool)], bounds: Option<&str>) -> Entity {
let bounds = bounds.map(|bounds| DataType::Record(ident(bounds)));
resource_entity_with_bounds_type(name, capacities, bounds)
}

fn resource_entity_with_bounds_type(
name: &str,
capacities: &[(&str, &str, bool)],
bounds: Option<DataType>,
) -> Entity {
let mut builder = EntityBuilder::new(ident(name))
.with_annotations(resource_annotations(definition_data(capacities)));
let fields = bounds
.map(|bounds| field("bounds", DataType::Record(ident(bounds))))
.into_iter();
let fields = bounds.map(|bounds| field("bounds", bounds)).into_iter();
builder = builder.try_with_event(event("operating", fields)).unwrap();
builder.build().unwrap()
}
Expand All @@ -81,13 +88,17 @@ fn resource_entity(name: &str, capacities: &[(&str, &str, bool)], bounds: Option
/// `on_ref`.
fn user_entity(name: &str, fsm: bool, record: &str, on_ref: bool) -> Entity {
let record = DataType::Record(ident(record));
user_entity_with_data_type(name, fsm, record, on_ref)
}

fn user_entity_with_data_type(name: &str, fsm: bool, data: DataType, on_ref: bool) -> Entity {
let ty = if on_ref {
DataType::EntityRef {
data: Some(Box::new(record)),
data: Some(Box::new(data)),
annotations: Annotations::default(),
}
} else {
record
data
};
let mut builder = EntityBuilder::new(ident(name))
.try_with_event(event("using", [field("claim", ty)]))
Expand Down Expand Up @@ -242,6 +253,79 @@ fn bounds_record_used_by_a_foreign_entity_is_rejected() {
));
}

#[test]
fn optional_resource_records_are_accepted() {
let memory = resource_entity_with_bounds_type(
"Memory",
&[("bytes", "occupancy", true)],
Some(DataType::Option(Box::new(DataType::Record(ident(
"MemoryBounds",
))))),
);
let worker = user_entity_with_data_type(
"Worker",
true,
DataType::Option(Box::new(DataType::Record(ident("MemoryUsage")))),
true,
);
let bounds = bounds_record("MemoryBounds", "Memory", &["bytes"]);
let usage = usage_record("MemoryUsage", "Memory", &["bytes"]);

assert!(resource_errors(&schema("App", [memory, worker], [bounds, usage])).is_empty());
}

#[test]
fn listed_resource_references_with_usage_are_accepted() {
let memory = resource_entity("Memory", &[("bytes", "occupancy", false)], None);
let reference = DataType::EntityRef {
data: Some(Box::new(DataType::Record(ident("MemoryUsage")))),
annotations: Annotations::default(),
};
let worker =
user_entity_with_data_type("Worker", true, DataType::List(Box::new(reference)), false);
let usage = usage_record("MemoryUsage", "Memory", &["bytes"]);

assert!(resource_errors(&schema("App", [memory, worker], [usage])).is_empty());
}

#[test]
fn usage_records_in_list_valued_reference_data_are_rejected() {
let memory = resource_entity("Memory", &[("bytes", "occupancy", false)], None);
let worker = user_entity_with_data_type(
"Worker",
true,
DataType::List(Box::new(DataType::Record(ident("MemoryUsage")))),
true,
);
let usage = usage_record("MemoryUsage", "Memory", &["bytes"]);
let errors = resource_errors(&schema("App", [memory, worker], [usage]));

assert!(
errors
.iter()
.any(|error| matches!(error, ResourceError::UsageInList { .. }))
);
}

#[test]
fn listed_bounds_records_are_rejected() {
let memory = resource_entity_with_bounds_type(
"Memory",
&[("bytes", "occupancy", true)],
Some(DataType::List(Box::new(DataType::Record(ident(
"MemoryBounds",
))))),
);
let bounds = bounds_record("MemoryBounds", "Memory", &["bytes"]);
let errors = resource_errors(&schema("App", [memory], [bounds]));

assert!(
errors
.iter()
.any(|error| matches!(error, ResourceError::BoundsInList { .. }))
);
}

fn assert_misplaced_resource(case: &str, schema: &Schema) {
assert!(
resource_errors(schema)
Expand Down