Add unknown ranges and open object values
This commit is contained in:
@@ -5,10 +5,16 @@ use std::{
|
||||
};
|
||||
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId,
|
||||
Span, format_diagnostic_with,
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, SourceId, Span,
|
||||
format_diagnostic_with,
|
||||
};
|
||||
|
||||
struct SourceInput {
|
||||
key: String,
|
||||
name: String,
|
||||
source: String,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
@@ -69,7 +75,7 @@ fn materialize_path(path: &str) -> Result<Data, String> {
|
||||
.map_err(|error| engine.format_diagnostic(&error))
|
||||
}
|
||||
|
||||
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
||||
fn read_root_source(path: &str) -> Result<SourceInput, Diagnostic> {
|
||||
if path == "-" {
|
||||
use std::io::Read;
|
||||
let mut source = String::new();
|
||||
@@ -82,7 +88,7 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
||||
format!("failed to read stdin: {error}"),
|
||||
)
|
||||
})?;
|
||||
Ok(LoadedSource {
|
||||
Ok(SourceInput {
|
||||
key: String::from("<stdin>"),
|
||||
name: String::from("<stdin>"),
|
||||
source,
|
||||
@@ -112,11 +118,11 @@ impl ImportLoader for FsLoader {
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
};
|
||||
load_path(&path).map(LoadedImport::Source)
|
||||
load_path(&path).map(|source| LoadedImport::source(source.key, source.name, source.source))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
|
||||
fn load_path(path: &Path) -> Result<SourceInput, Diagnostic> {
|
||||
let canonical = path.canonicalize().map_err(|error| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
@@ -132,7 +138,7 @@ fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
|
||||
)
|
||||
})?;
|
||||
let key = canonical.to_string_lossy().into_owned();
|
||||
Ok(LoadedSource {
|
||||
Ok(SourceInput {
|
||||
name: key.clone(),
|
||||
key,
|
||||
source,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use decodal::{Data, EmptyLoader, Engine, HostValue};
|
||||
use decodal::{Data, EmptyLoader, Engine, Value};
|
||||
|
||||
fn main() -> decodal::Result<()> {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
|
||||
engine.bind_global(
|
||||
"Service",
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
||||
("enabled", HostValue::bool_type().default_bool(true)?),
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||
("enabled", Value::bool_type().default_bool(true)?),
|
||||
]),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||
};
|
||||
use decodal::{Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value};
|
||||
|
||||
const POST: &str = r#"---
|
||||
title: Hello
|
||||
@@ -33,22 +31,22 @@ impl ImportLoader for ContentLoader {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(source: &str) -> decodal::Result<HostValue> {
|
||||
fn parse_markdown(source: &str) -> decodal::Result<Value> {
|
||||
let source = source.strip_prefix("---\n").ok_or_else(frontmatter_error)?;
|
||||
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?;
|
||||
let mut fields = Vec::new();
|
||||
for line in frontmatter.lines() {
|
||||
let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?;
|
||||
let value = match value.trim() {
|
||||
"true" => HostValue::bool(true),
|
||||
"false" => HostValue::bool(false),
|
||||
value => HostValue::string(value),
|
||||
"true" => Value::bool(true),
|
||||
"false" => Value::bool(false),
|
||||
value => Value::string(value),
|
||||
};
|
||||
fields.push((name.trim(), value));
|
||||
}
|
||||
Ok(HostValue::object([
|
||||
("frontmatter", HostValue::object(fields)),
|
||||
("body", HostValue::string(body)),
|
||||
Ok(Value::object([
|
||||
("frontmatter", Value::object(fields)),
|
||||
("body", Value::string(body)),
|
||||
]))
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ pub struct SpannedExpr {
|
||||
pub enum Expr {
|
||||
Literal(Literal),
|
||||
Ident(String),
|
||||
Object(Vec<Field>),
|
||||
Object {
|
||||
fields: Vec<Field>,
|
||||
rest: Option<ObjectRest>,
|
||||
},
|
||||
Array(Vec<ExprId>),
|
||||
ArrayConstraint {
|
||||
item: ExprId,
|
||||
@@ -116,6 +119,12 @@ pub struct Field {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectRest {
|
||||
pub value: ExprId,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Param {
|
||||
pub name: String,
|
||||
|
||||
@@ -19,6 +19,10 @@ pub fn normalize_constraints(
|
||||
|
||||
for entry in constraints {
|
||||
match entry.constraint {
|
||||
Constraint::Unknown => rest.push(ConstraintEntry {
|
||||
constraint: Constraint::Unknown,
|
||||
span: entry.span,
|
||||
}),
|
||||
Constraint::Type(next) => match primitive {
|
||||
Some((current, current_span)) if current != next => {
|
||||
return Err(Diagnostic::new(
|
||||
|
||||
+483
-81
@@ -5,14 +5,15 @@ use crate::{
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
|
||||
constraints::normalize_constraints,
|
||||
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
||||
embedding::HostValue,
|
||||
module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module},
|
||||
module::{EmptyLoader, ImportLoader, LoadedImport, Module},
|
||||
parse_source_with_source_id,
|
||||
runtime::{
|
||||
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
|
||||
EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField,
|
||||
ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
|
||||
ObjectRest as RuntimeObjectRest, ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId,
|
||||
ThunkKind, ThunkState,
|
||||
},
|
||||
value::Value,
|
||||
};
|
||||
|
||||
pub struct Engine<L = EmptyLoader> {
|
||||
@@ -94,8 +95,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_global(&mut self, name: impl Into<String>, value: HostValue) -> Result<ThunkId> {
|
||||
let value = self.internalize_host_value(value)?;
|
||||
pub fn bind_global(&mut self, name: impl Into<String>, value: Value) -> Result<ThunkId> {
|
||||
let value = self.internalize_value(value)?;
|
||||
let thunk = self.add_value_thunk(value);
|
||||
self.bind(self.prelude_env, name.into(), thunk);
|
||||
Ok(thunk)
|
||||
@@ -280,9 +281,9 @@ impl<L: ImportLoader> Engine<L> {
|
||||
let module = ModuleId(self.modules.len() as u32);
|
||||
let root_env = self.new_env(Some(self.prelude_env));
|
||||
let root_thunk = if source_form == SourceForm::Fields {
|
||||
if let Expr::Object(fields) = ast.get(root).expr.clone() {
|
||||
if let Expr::Object { fields, rest } = ast.get(root).expr.clone() {
|
||||
let object = self
|
||||
.build_object(module, &fields, root_env)
|
||||
.build_object(module, &fields, rest.as_ref(), root_env)
|
||||
.expect("module object construction should not fail for parsed fields");
|
||||
for field in &object.fields {
|
||||
self.bind(root_env, field.name.clone(), field.value);
|
||||
@@ -318,14 +319,14 @@ impl<L: ImportLoader> Engine<L> {
|
||||
let current_key = self.modules[current.0 as usize].key.clone();
|
||||
let loaded = self.loader.load(Some(¤t_key), specifier)?;
|
||||
match loaded {
|
||||
LoadedImport::Source(LoadedSource { key, name, source }) => {
|
||||
LoadedImport::Source { key, name, source } => {
|
||||
if let Some(value) = self.find_imported_value(&key) {
|
||||
return self.force(value);
|
||||
}
|
||||
let module = self.add_source(key, name, &source)?;
|
||||
self.eval_module(module)
|
||||
}
|
||||
LoadedImport::Value(LoadedValue { key, value }) => {
|
||||
LoadedImport::Value { key, value } => {
|
||||
if let Some(module) = self.find_module(&key) {
|
||||
return self.eval_module(module);
|
||||
}
|
||||
@@ -333,7 +334,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
return self.force(value);
|
||||
}
|
||||
let origin = ImportedValueOrigin::root(key.clone());
|
||||
let value = self.internalize_host_value_with_origin(value, Some(&origin))?;
|
||||
let value = self.internalize_value_with_origin(value, Some(&origin))?;
|
||||
let root = self.add_value_thunk_with_import_origin(value, origin);
|
||||
self.imported_values.push(ImportedValue { key, root });
|
||||
self.force(root)
|
||||
@@ -354,8 +355,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
match expr {
|
||||
Expr::Literal(literal) => Ok(RuntimeValue::Concrete(literal_to_concrete(literal))),
|
||||
Expr::Ident(name) => self.eval_ident(&name, env, span),
|
||||
Expr::Object(fields) => self
|
||||
.build_object(reference.module, &fields, env)
|
||||
Expr::Object { fields, rest } => self
|
||||
.build_object(reference.module, &fields, rest.as_ref(), env)
|
||||
.map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))),
|
||||
Expr::Array(items) => {
|
||||
let thunks = items
|
||||
@@ -699,6 +700,15 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
|
||||
fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
|
||||
if name == "Unknown" {
|
||||
return Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![ConstraintEntry {
|
||||
constraint: Constraint::Unknown,
|
||||
span,
|
||||
}],
|
||||
default: None,
|
||||
}));
|
||||
}
|
||||
if let Some(primitive) = primitive_type(name) {
|
||||
return Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![ConstraintEntry {
|
||||
@@ -738,9 +748,23 @@ impl<L: ImportLoader> Engine<L> {
|
||||
&mut self,
|
||||
module: ModuleId,
|
||||
fields: &[Field],
|
||||
rest: Option<&crate::ast::ObjectRest>,
|
||||
env: EnvId,
|
||||
) -> Result<ObjectValue> {
|
||||
let mut object = ObjectValue { fields: Vec::new() };
|
||||
let mut object = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: rest.map(|rest| RuntimeObjectRest {
|
||||
value: self.add_expr_thunk_with_span(
|
||||
ExprRef {
|
||||
module,
|
||||
expr: rest.value,
|
||||
},
|
||||
env,
|
||||
rest.span,
|
||||
),
|
||||
span: rest.span,
|
||||
}),
|
||||
};
|
||||
for field in fields {
|
||||
self.insert_field(
|
||||
&mut object,
|
||||
@@ -805,7 +829,10 @@ impl<L: ImportLoader> Engine<L> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut nested = ObjectValue { fields: Vec::new() };
|
||||
let mut nested = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: None,
|
||||
};
|
||||
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
|
||||
object.fields.push(ObjectField {
|
||||
name: name.clone(),
|
||||
@@ -1032,6 +1059,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
) -> Result<()> {
|
||||
for wider_entry in &wider.constraints {
|
||||
let implied = match &wider_entry.constraint {
|
||||
Constraint::Unknown => true,
|
||||
Constraint::Type(wider_type) => narrower.constraints.iter().any(|entry| {
|
||||
matches!(entry.constraint, Constraint::Type(narrower_type) if narrower_type == *wider_type)
|
||||
}),
|
||||
@@ -1145,11 +1173,20 @@ impl<L: ImportLoader> Engine<L> {
|
||||
));
|
||||
};
|
||||
|
||||
for narrower_field in &narrower.fields {
|
||||
if wider
|
||||
.fields
|
||||
let ObjectValue {
|
||||
fields: narrower_fields,
|
||||
rest: narrower_rest,
|
||||
} = narrower;
|
||||
let ObjectValue {
|
||||
fields: wider_fields,
|
||||
rest: wider_rest,
|
||||
} = wider;
|
||||
|
||||
for narrower_field in &narrower_fields {
|
||||
if wider_fields
|
||||
.iter()
|
||||
.all(|wider_field| wider_field.name != narrower_field.name)
|
||||
&& wider_rest.is_none()
|
||||
{
|
||||
let origin = self.thunk_import_origin(narrower_field.value).cloned();
|
||||
let diagnostic_span = if origin.is_some() {
|
||||
@@ -1172,10 +1209,33 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut refined = ObjectValue { fields: Vec::new() };
|
||||
for wider_field in wider.fields {
|
||||
let Some(narrower_field) = narrower
|
||||
.fields
|
||||
if let Some(narrower_rest) = &narrower_rest
|
||||
&& wider_rest.is_none()
|
||||
{
|
||||
let diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::ConstraintViolation,
|
||||
source_span_or(narrower_rest.span, narrower_diagnostic_span),
|
||||
"left side of `as` permits additional fields but the right side is closed",
|
||||
)
|
||||
.with_label(operation_span, "`as` applied here");
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
narrower_rest.span,
|
||||
"narrower object rest range declared here",
|
||||
);
|
||||
return Err(with_source_label(
|
||||
diagnostic,
|
||||
wider_span,
|
||||
"closed wider object declared here",
|
||||
));
|
||||
}
|
||||
|
||||
let mut refined = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: None,
|
||||
};
|
||||
for wider_field in wider_fields.iter().cloned() {
|
||||
let Some(narrower_field) = narrower_fields
|
||||
.iter()
|
||||
.find(|narrower_field| narrower_field.name == wider_field.name)
|
||||
else {
|
||||
@@ -1227,6 +1287,82 @@ impl<L: ImportLoader> Engine<L> {
|
||||
span: narrower_field.span,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(wider_rest) = &wider_rest {
|
||||
let wider_rest_value = self.force(wider_rest.value)?;
|
||||
for narrower_field in narrower_fields.iter().filter(|narrower_field| {
|
||||
wider_fields
|
||||
.iter()
|
||||
.all(|wider_field| wider_field.name != narrower_field.name)
|
||||
}) {
|
||||
let narrower_origin = self.thunk_import_origin(narrower_field.value).cloned();
|
||||
let narrower_value = self.force(narrower_field.value)?;
|
||||
let field_name = narrower_field.name.clone();
|
||||
let narrowed = self
|
||||
.apply_as(
|
||||
narrower_value.clone(),
|
||||
wider_rest_value.clone(),
|
||||
operation_span,
|
||||
narrower_field.span,
|
||||
wider_rest.span,
|
||||
)
|
||||
.map_err(|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
wider_rest.span,
|
||||
"wider object rest range declared here",
|
||||
);
|
||||
if narrower_origin.is_some() {
|
||||
annotate_imported_value(
|
||||
diagnostic,
|
||||
narrower_origin.as_ref(),
|
||||
Some(&narrower_value),
|
||||
)
|
||||
} else {
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
narrower_field.span,
|
||||
format!("additional field `{field_name}` checked here"),
|
||||
)
|
||||
}
|
||||
})?;
|
||||
refined.fields.push(ObjectField {
|
||||
name: field_name,
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
narrowed,
|
||||
narrower_field.span,
|
||||
narrower_origin,
|
||||
),
|
||||
span: narrower_field.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refined.rest = match (narrower_rest, wider_rest) {
|
||||
(Some(narrower_rest), Some(wider_rest)) => {
|
||||
let narrower_origin = self.thunk_import_origin(narrower_rest.value).cloned();
|
||||
let narrower_value = self.force(narrower_rest.value)?;
|
||||
let wider_value = self.force(wider_rest.value)?;
|
||||
let narrowed = self.apply_as(
|
||||
narrower_value,
|
||||
wider_value,
|
||||
operation_span,
|
||||
narrower_rest.span,
|
||||
wider_rest.span,
|
||||
)?;
|
||||
Some(RuntimeObjectRest {
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
narrowed,
|
||||
narrower_rest.span,
|
||||
narrower_origin,
|
||||
),
|
||||
span: narrower_rest.span,
|
||||
})
|
||||
}
|
||||
(None, Some(wider_rest)) => Some(wider_rest),
|
||||
(None, None) => None,
|
||||
(Some(_), None) => unreachable!("open narrower object was rejected above"),
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(refined)))
|
||||
}
|
||||
|
||||
@@ -1321,7 +1457,46 @@ impl<L: ImportLoader> Engine<L> {
|
||||
rhs: ObjectValue,
|
||||
span: Span,
|
||||
) -> Result<RuntimeValue> {
|
||||
for rhs_field in rhs.fields {
|
||||
let lhs_rest = lhs.rest.take();
|
||||
let rhs_rest = rhs.rest;
|
||||
let rhs_field_names = rhs
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| field.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(rhs_rest) = &rhs_rest {
|
||||
let rhs_rest_value = self.force(rhs_rest.value)?;
|
||||
for lhs_field in lhs
|
||||
.fields
|
||||
.iter_mut()
|
||||
.filter(|field| !rhs_field_names.contains(&field.name))
|
||||
{
|
||||
let lhs_origin = self.thunk_import_origin(lhs_field.value).cloned();
|
||||
let lhs_value = self.force(lhs_field.value)?;
|
||||
let value = self
|
||||
.compose_and(lhs_value.clone(), rhs_rest_value.clone(), span)
|
||||
.map_err(|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
rhs_rest.span,
|
||||
"right object rest constraint declared here",
|
||||
);
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
lhs_field.span,
|
||||
format!("left field `{}` constrained here", lhs_field.name),
|
||||
)
|
||||
})?;
|
||||
lhs_field.value = self.add_value_thunk_with_span_and_import_origin(
|
||||
value,
|
||||
lhs_field.span,
|
||||
lhs_origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for mut rhs_field in rhs.fields {
|
||||
if let Some(index) = lhs
|
||||
.fields
|
||||
.iter()
|
||||
@@ -1381,9 +1556,65 @@ impl<L: ImportLoader> Engine<L> {
|
||||
lhs.fields[index].span = rhs_field.span;
|
||||
}
|
||||
} else {
|
||||
if let Some(lhs_rest) = &lhs_rest {
|
||||
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
|
||||
let rhs_value = self.force(rhs_field.value)?;
|
||||
let lhs_rest_value = self.force(lhs_rest.value)?;
|
||||
let value = self.compose_and(rhs_value, lhs_rest_value, span).map_err(
|
||||
|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
lhs_rest.span,
|
||||
"left object rest constraint declared here",
|
||||
);
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
rhs_field.span,
|
||||
format!("right field `{}` constrained here", rhs_field.name),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
rhs_field.value = self.add_value_thunk_with_span_and_import_origin(
|
||||
value,
|
||||
rhs_field.span,
|
||||
rhs_origin,
|
||||
);
|
||||
}
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
}
|
||||
|
||||
lhs.rest = match (lhs_rest, rhs_rest) {
|
||||
(Some(lhs_rest), Some(rhs_rest)) => {
|
||||
let lhs_origin = self.thunk_import_origin(lhs_rest.value).cloned();
|
||||
let lhs_value = self.force(lhs_rest.value)?;
|
||||
let rhs_value = self.force(rhs_rest.value)?;
|
||||
let value = self
|
||||
.compose_and(lhs_value, rhs_value, span)
|
||||
.map_err(|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
lhs_rest.span,
|
||||
"left object rest constraint declared here",
|
||||
);
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
rhs_rest.span,
|
||||
"right object rest constraint declared here",
|
||||
)
|
||||
})?;
|
||||
Some(RuntimeObjectRest {
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
value,
|
||||
rhs_rest.span,
|
||||
lhs_origin,
|
||||
),
|
||||
span: rhs_rest.span,
|
||||
})
|
||||
}
|
||||
(Some(rest), None) | (None, Some(rest)) => Some(rest),
|
||||
(None, None) => None,
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
|
||||
}
|
||||
|
||||
@@ -1398,6 +1629,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
|
||||
fn patch_objects(&mut self, mut lhs: ObjectValue, rhs: ObjectValue) -> Result<RuntimeValue> {
|
||||
let rhs_rest = rhs.rest;
|
||||
for rhs_field in rhs.fields {
|
||||
if let Some(index) = lhs
|
||||
.fields
|
||||
@@ -1418,6 +1650,9 @@ impl<L: ImportLoader> Engine<L> {
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
}
|
||||
if rhs_rest.is_some() {
|
||||
lhs.rest = rhs_rest;
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
|
||||
}
|
||||
|
||||
@@ -1459,6 +1694,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
constraint.span
|
||||
};
|
||||
match constraint_value {
|
||||
Constraint::Unknown => Ok(value),
|
||||
Constraint::Type(primitive) => {
|
||||
if value_matches_primitive(&value, *primitive) {
|
||||
Ok(value)
|
||||
@@ -1547,8 +1783,12 @@ impl<L: ImportLoader> Engine<L> {
|
||||
));
|
||||
};
|
||||
let value_schema = self.force(*value_constraint)?;
|
||||
let mut constrained_fields = Vec::with_capacity(object.fields.len());
|
||||
for field in object.fields {
|
||||
let ObjectValue {
|
||||
fields,
|
||||
rest: object_rest,
|
||||
} = object;
|
||||
let mut constrained_fields = Vec::with_capacity(fields.len());
|
||||
for field in fields {
|
||||
let field_span = self.thunk_span(field.value);
|
||||
let field_origin = self.thunk_import_origin(field.value).cloned();
|
||||
let field_value = self.force(field.value)?;
|
||||
@@ -1592,8 +1832,30 @@ impl<L: ImportLoader> Engine<L> {
|
||||
span: field.span,
|
||||
});
|
||||
}
|
||||
let constrained_rest = if let Some(object_rest) = object_rest {
|
||||
let rest_origin = self.thunk_import_origin(object_rest.value).cloned();
|
||||
let rest_value = self.force(object_rest.value)?;
|
||||
let constrained = self.apply_as(
|
||||
rest_value,
|
||||
value_schema,
|
||||
span,
|
||||
object_rest.span,
|
||||
constraint_span,
|
||||
)?;
|
||||
Some(RuntimeObjectRest {
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
constrained,
|
||||
object_rest.span,
|
||||
rest_origin,
|
||||
),
|
||||
span: object_rest.span,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(ObjectValue {
|
||||
fields: constrained_fields,
|
||||
rest: constrained_rest,
|
||||
})))
|
||||
}
|
||||
Constraint::Compare(op, expected) => compare_value(&value, *op, expected)
|
||||
@@ -1671,40 +1933,39 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
||||
self.internalize_host_value_with_origin(value, None)
|
||||
fn internalize_value(&mut self, value: Value) -> Result<RuntimeValue> {
|
||||
self.internalize_value_with_origin(value, None)
|
||||
}
|
||||
|
||||
fn internalize_host_value_with_origin(
|
||||
fn internalize_value_with_origin(
|
||||
&mut self,
|
||||
value: HostValue,
|
||||
value: Value,
|
||||
origin: Option<&ImportedValueOrigin>,
|
||||
) -> Result<RuntimeValue> {
|
||||
match value {
|
||||
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||
HostValue::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
|
||||
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||
HostValue::Array(items) => {
|
||||
Value::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||
Value::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||
Value::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
|
||||
Value::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||
Value::Array(items) => {
|
||||
let mut thunks = Vec::new();
|
||||
for (index, item) in items.into_iter().enumerate() {
|
||||
let item_origin = origin.map(|origin| origin.index(index));
|
||||
let value =
|
||||
self.internalize_host_value_with_origin(item, item_origin.as_ref())?;
|
||||
let value = self.internalize_value_with_origin(item, item_origin.as_ref())?;
|
||||
thunks
|
||||
.push(self.add_value_thunk_with_optional_import_origin(value, item_origin));
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
||||
}
|
||||
HostValue::ArrayConstraint {
|
||||
Value::ArrayRange {
|
||||
item,
|
||||
mut constraints,
|
||||
default,
|
||||
} => {
|
||||
let item = self.internalize_host_value_with_origin(*item, origin)?;
|
||||
let item = self.internalize_value_with_origin(*item, origin)?;
|
||||
let item = self.add_value_thunk_with_optional_import_origin(item, origin.cloned());
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
let value = self.internalize_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
@@ -1723,16 +1984,16 @@ impl<L: ImportLoader> Engine<L> {
|
||||
default,
|
||||
}))
|
||||
}
|
||||
HostValue::MapConstraint {
|
||||
Value::MapRange {
|
||||
value,
|
||||
mut constraints,
|
||||
default,
|
||||
} => {
|
||||
let value = self.internalize_host_value_with_origin(*value, origin)?;
|
||||
let value = self.internalize_value_with_origin(*value, origin)?;
|
||||
let value =
|
||||
self.add_value_thunk_with_optional_import_origin(value, origin.cloned());
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
let value = self.internalize_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
@@ -1751,8 +2012,11 @@ impl<L: ImportLoader> Engine<L> {
|
||||
default,
|
||||
}))
|
||||
}
|
||||
HostValue::Object(fields) => {
|
||||
let mut object = ObjectValue { fields: Vec::new() };
|
||||
Value::Object { fields, rest } => {
|
||||
let mut object = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: None,
|
||||
};
|
||||
for field in fields {
|
||||
let field_origin = origin.map(|origin| origin.field(field.name.clone()));
|
||||
if object
|
||||
@@ -1763,7 +2027,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
let diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
Span::default(),
|
||||
format!("duplicate host object field `{}`", field.name),
|
||||
format!("duplicate object field `{}`", field.name),
|
||||
);
|
||||
return Err(annotate_imported_value(
|
||||
diagnostic,
|
||||
@@ -1771,8 +2035,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
None,
|
||||
));
|
||||
}
|
||||
let value = self
|
||||
.internalize_host_value_with_origin(field.value, field_origin.as_ref())?;
|
||||
let value =
|
||||
self.internalize_value_with_origin(field.value, field_origin.as_ref())?;
|
||||
let value =
|
||||
self.add_value_thunk_with_optional_import_origin(value, field_origin);
|
||||
object.fields.push(ObjectField {
|
||||
@@ -1781,14 +2045,22 @@ impl<L: ImportLoader> Engine<L> {
|
||||
span: Span::default(),
|
||||
});
|
||||
}
|
||||
if let Some(rest) = rest {
|
||||
let rest = self.internalize_value_with_origin(*rest, origin)?;
|
||||
object.rest = Some(RuntimeObjectRest {
|
||||
value: self
|
||||
.add_value_thunk_with_optional_import_origin(rest, origin.cloned()),
|
||||
span: Span::default(),
|
||||
});
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
|
||||
}
|
||||
HostValue::Abstract {
|
||||
Value::Range {
|
||||
constraints,
|
||||
default,
|
||||
} => {
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
let value = self.internalize_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
@@ -2252,6 +2524,7 @@ fn upper_bound_implies(
|
||||
|
||||
fn constraint_description(constraint: &Constraint) -> String {
|
||||
match constraint {
|
||||
Constraint::Unknown => String::from("the unknown top range"),
|
||||
Constraint::Type(primitive) => format!("type {primitive:?}"),
|
||||
Constraint::ArrayItems(_) => String::from("an array element range"),
|
||||
Constraint::MapValues(_) => String::from("a map value range"),
|
||||
@@ -2590,7 +2863,7 @@ fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{LoadedSource, parse_source};
|
||||
use crate::parse_source;
|
||||
|
||||
fn eval_data(source: &str) -> Data {
|
||||
let parsed = parse_source(source).unwrap();
|
||||
@@ -2887,6 +3160,29 @@ mod tests {
|
||||
assert!(value.default.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_is_the_unmaterializable_top_range() {
|
||||
assert_eq!(eval_data("42 as Unknown"), Data::Int(42));
|
||||
assert_eq!(eval_data("Unknown default {}"), Data::Object(Vec::new()));
|
||||
|
||||
let parsed = parse_source("Unknown").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let value = engine.eval_root().unwrap();
|
||||
let error = engine.materialize(&value).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Materialize);
|
||||
assert!(error.message.contains("unresolved abstract value"));
|
||||
|
||||
let parsed = parse_source("Unknown as Int").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert!(error.message.contains("not known to be narrower"));
|
||||
|
||||
let parsed = parse_source("{...Unknown}").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let value = engine.eval_root().unwrap();
|
||||
assert!(engine.materialize(&value).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abstract_collection_ranges_can_be_proven_narrower() {
|
||||
let parsed = parse_source("[...(Int & > 10)] as [...Int]").unwrap();
|
||||
@@ -2980,6 +3276,100 @@ mod tests {
|
||||
assert_eq!(eval_data("{} as {...String}"), Data::Object(vec![]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_rest_constraint_accepts_and_preserves_additional_fields() {
|
||||
let data = eval_data(
|
||||
r#"
|
||||
{
|
||||
hoge = 1;
|
||||
fuga = "two";
|
||||
extra = true;
|
||||
} as {
|
||||
hoge = Int;
|
||||
fuga = String;
|
||||
...Unknown
|
||||
}
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
data,
|
||||
Data::Object(vec![
|
||||
DataField {
|
||||
name: String::from("hoge"),
|
||||
value: Data::Int(1),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("fuga"),
|
||||
value: Data::String(String::from("two")),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("extra"),
|
||||
value: Data::Bool(true),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
eval_data("{ hoge = 1; ...Unknown }"),
|
||||
Data::Object(vec![DataField {
|
||||
name: String::from("hoge"),
|
||||
value: Data::Int(1),
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_rest_constraint_rejects_invalid_additional_fields() {
|
||||
let parsed =
|
||||
parse_source(r#"{ hoge = 1; extra = "no"; } as { hoge = Int; ...Int }"#).unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
|
||||
assert!(error.message.contains("expected Int"));
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message.contains("object rest range"))
|
||||
);
|
||||
|
||||
let parsed = parse_source("{ hoge = 1; ...Unknown } as { hoge = Int; }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert!(error.message.contains("permits additional fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_rest_constraints_compose_as_ranges() {
|
||||
assert_eq!(
|
||||
eval_data("{ fixed = 1; ...Unknown } & { additional = 2; ...Unknown }"),
|
||||
Data::Object(vec![
|
||||
DataField {
|
||||
name: String::from("fixed"),
|
||||
value: Data::Int(1),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("additional"),
|
||||
value: Data::Int(2),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
let parsed =
|
||||
parse_source("{ fixed = Int; ...Int } as { fixed = Unknown; ...Unknown }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
assert!(matches!(
|
||||
engine.eval_root().unwrap(),
|
||||
RuntimeValue::Concrete(ConcreteValue::Object(_))
|
||||
));
|
||||
|
||||
let parsed =
|
||||
parse_source("{ fixed = Int; ...Unknown } as { fixed = Unknown; ...Int }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert!(error.message.contains("not known to be narrower"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_array_primitive_reports_migration() {
|
||||
let parsed = parse_source("Array").unwrap();
|
||||
@@ -3100,11 +3490,11 @@ mod tests {
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
|
||||
})?;
|
||||
Ok(LoadedImport::Source(LoadedSource {
|
||||
Ok(LoadedImport::Source {
|
||||
key: specifier.into(),
|
||||
name: specifier.into(),
|
||||
source,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3147,7 +3537,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_test_markdown(source: &str) -> Result<HostValue> {
|
||||
fn parse_test_markdown(source: &str) -> Result<Value> {
|
||||
let source = source.strip_prefix("---\n").ok_or_else(|| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
@@ -3172,15 +3562,15 @@ mod tests {
|
||||
)
|
||||
})?;
|
||||
let value = match value.trim() {
|
||||
"true" => HostValue::bool(true),
|
||||
"false" => HostValue::bool(false),
|
||||
value => HostValue::string(value.trim_matches('"')),
|
||||
"true" => Value::bool(true),
|
||||
"false" => Value::bool(false),
|
||||
value => Value::string(value.trim_matches('"')),
|
||||
};
|
||||
fields.push((name.trim(), value));
|
||||
}
|
||||
Ok(HostValue::object([
|
||||
("frontmatter", HostValue::object(fields)),
|
||||
("body", HostValue::string(body)),
|
||||
Ok(Value::object([
|
||||
("frontmatter", Value::object(fields)),
|
||||
("body", Value::string(body)),
|
||||
]))
|
||||
}
|
||||
|
||||
@@ -3275,9 +3665,9 @@ mod tests {
|
||||
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||
Ok(LoadedImport::value(
|
||||
"content/navigation.json",
|
||||
HostValue::object([(
|
||||
Value::object([(
|
||||
"items",
|
||||
HostValue::array([HostValue::string("home"), HostValue::int(2)]),
|
||||
Value::array([Value::string("home"), Value::int(2)]),
|
||||
)]),
|
||||
))
|
||||
}
|
||||
@@ -3308,7 +3698,7 @@ mod tests {
|
||||
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||
Ok(LoadedImport::value(
|
||||
"shared-value",
|
||||
HostValue::int_type().default_int(1)?,
|
||||
Value::int_type().default_int(1)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -3351,16 +3741,10 @@ mod tests {
|
||||
engine
|
||||
.bind_global(
|
||||
"Service",
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
(
|
||||
"port",
|
||||
HostValue::int_type().gt(443).default_int(8443).unwrap(),
|
||||
),
|
||||
(
|
||||
"enabled",
|
||||
HostValue::bool_type().default_bool(true).unwrap(),
|
||||
),
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443).unwrap()),
|
||||
("enabled", Value::bool_type().default_bool(true).unwrap()),
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -3385,12 +3769,9 @@ mod tests {
|
||||
engine
|
||||
.bind_global(
|
||||
"Services",
|
||||
HostValue::map_of(HostValue::object([
|
||||
("port", HostValue::int_type()),
|
||||
(
|
||||
"enabled",
|
||||
HostValue::bool_type().default_bool(true).unwrap(),
|
||||
),
|
||||
Value::map_of(Value::object([
|
||||
("port", Value::int_type()),
|
||||
("enabled", Value::bool_type().default_bool(true).unwrap()),
|
||||
])),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -3410,14 +3791,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_schema_errors_point_to_the_dcdl_application() {
|
||||
fn host_prelude_provides_object_rest_and_unknown_ranges() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global(
|
||||
"Service",
|
||||
HostValue::object([("port", HostValue::int_type())]),
|
||||
"OpenService",
|
||||
Value::object_with_rest([("name", Value::string_type())], Value::unknown()),
|
||||
)
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"main",
|
||||
"main",
|
||||
r#"{ name = "api"; metadata = { owner = "platform"; }; } as OpenService"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let Data::Object(fields) = data else { panic!() };
|
||||
assert_eq!(fields.len(), 2);
|
||||
assert_eq!(fields[0].value, Data::String(String::from("api")));
|
||||
assert!(matches!(fields[1].value, Data::Object(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_schema_errors_point_to_the_dcdl_application() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global("Service", Value::object([("port", Value::int_type())]))
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source("main", "main", r#"{ port = "not-an-int"; } as Service"#)
|
||||
.unwrap();
|
||||
@@ -3435,7 +3837,7 @@ mod tests {
|
||||
#[test]
|
||||
fn module_binding_shadows_host_prelude() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine.bind_global("Service", HostValue::int(1)).unwrap();
|
||||
engine.bind_global("Service", Value::int(1)).unwrap();
|
||||
let module = engine
|
||||
.add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
|
||||
.unwrap();
|
||||
|
||||
@@ -5,7 +5,6 @@ extern crate alloc;
|
||||
pub mod ast;
|
||||
pub mod constraints;
|
||||
pub mod diagnostic;
|
||||
pub mod embedding;
|
||||
pub mod environment;
|
||||
pub mod eval;
|
||||
mod lexer;
|
||||
@@ -14,29 +13,28 @@ pub mod parser;
|
||||
pub mod runtime;
|
||||
pub mod span;
|
||||
pub mod typed;
|
||||
pub mod value;
|
||||
|
||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, ObjectRest, Param};
|
||||
pub use constraints::normalize_constraints;
|
||||
#[cfg(feature = "derive")]
|
||||
pub use decodal_derive::Decodal;
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||
pub use embedding::{HostField, HostValue};
|
||||
pub use environment::HostEnvironment;
|
||||
pub use eval::{Engine, format_diagnostic_with};
|
||||
pub use lexer::{
|
||||
Token as SyntaxToken, TokenKind as SyntaxTokenKind, tokenize_source,
|
||||
tokenize_source_with_source_id,
|
||||
};
|
||||
pub use module::{
|
||||
EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module,
|
||||
};
|
||||
pub use module::{EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, Module};
|
||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType};
|
||||
pub use span::{SourceId, Span};
|
||||
pub use typed::{
|
||||
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
|
||||
DecodalDecode, DecodalRest, DecodalSchema, DecodeError, DecodeResult, IntoValue, data_at_path,
|
||||
decode_path, prefix_decode_error,
|
||||
};
|
||||
pub use value::{Value, ValueField};
|
||||
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
Ast, ExprId, HostValue, SourceForm, SourceId,
|
||||
Ast, ExprId, SourceForm, SourceId, Value,
|
||||
runtime::{EnvId, ThunkId},
|
||||
};
|
||||
|
||||
@@ -17,23 +17,18 @@ pub struct Module {
|
||||
pub root_thunk: ThunkId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedSource {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedValue {
|
||||
pub key: String,
|
||||
pub value: HostValue,
|
||||
}
|
||||
|
||||
/// Content resolved by an [`ImportLoader`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadedImport {
|
||||
Source(LoadedSource),
|
||||
Value(LoadedValue),
|
||||
Source {
|
||||
key: String,
|
||||
name: String,
|
||||
source: String,
|
||||
},
|
||||
Value {
|
||||
key: String,
|
||||
value: Value,
|
||||
},
|
||||
}
|
||||
|
||||
/// An import specifier offered by host-owned language tooling.
|
||||
@@ -63,18 +58,18 @@ impl LoadedImport {
|
||||
name: impl Into<String>,
|
||||
source: impl Into<String>,
|
||||
) -> Self {
|
||||
Self::Source(LoadedSource {
|
||||
Self::Source {
|
||||
key: key.into(),
|
||||
name: name.into(),
|
||||
source: source.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(key: impl Into<String>, value: HostValue) -> Self {
|
||||
Self::Value(LoadedValue {
|
||||
pub fn value(key: impl Into<String>, value: Value) -> Self {
|
||||
Self::Value {
|
||||
key: key.into(),
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
SourceId, Span,
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
|
||||
ast::{
|
||||
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, ObjectRest, Param,
|
||||
UnaryOp,
|
||||
},
|
||||
diagnostic::{Diagnostic, Result},
|
||||
lexer::{Lexer, Token, TokenKind},
|
||||
};
|
||||
@@ -59,7 +62,7 @@ impl Parser {
|
||||
})
|
||||
.unwrap_or_else(|| self.peek().span);
|
||||
(
|
||||
self.ast.push(Expr::Object(fields), span),
|
||||
self.ast.push(Expr::Object { fields, rest: None }, span),
|
||||
SourceForm::Fields,
|
||||
)
|
||||
} else {
|
||||
@@ -338,23 +341,38 @@ impl Parser {
|
||||
|
||||
let mut fields = Vec::new();
|
||||
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
||||
return Ok(self
|
||||
.ast
|
||||
.push(Expr::Object(fields), start_span.join(self.previous_span())));
|
||||
return Ok(self.ast.push(
|
||||
Expr::Object { fields, rest: None },
|
||||
start_span.join(self.previous_span()),
|
||||
));
|
||||
}
|
||||
let mut rest = None;
|
||||
loop {
|
||||
fields.push(self.parse_field()?);
|
||||
if self.consume_kind(&TokenKind::Semicolon).is_some() {
|
||||
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
||||
break;
|
||||
}
|
||||
if let Some(ellipsis) = self.consume_kind(&TokenKind::Ellipsis) {
|
||||
let value = self.parse_expr(0)?;
|
||||
rest = Some(ObjectRest {
|
||||
value,
|
||||
span: ellipsis.join(self.ast.span(value)),
|
||||
});
|
||||
let _ = self.consume_kind(&TokenKind::Semicolon);
|
||||
self.expect_kind(
|
||||
&TokenKind::RBrace,
|
||||
"expected '}' after object rest constraint",
|
||||
)?;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?;
|
||||
break;
|
||||
}
|
||||
let span = start_span.join(self.previous_span());
|
||||
Ok(self.ast.push(Expr::Object(fields), span))
|
||||
Ok(self.ast.push(Expr::Object { fields, rest }, span))
|
||||
}
|
||||
|
||||
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
|
||||
@@ -697,13 +715,16 @@ mod tests {
|
||||
#[test]
|
||||
fn parses_top_level_fields_as_object() {
|
||||
let parsed = parse_source("port = Int & >= 1 default 8080;").unwrap();
|
||||
assert!(matches!(parsed.ast.get(parsed.root).expr, Expr::Object(_)));
|
||||
assert!(matches!(
|
||||
parsed.ast.get(parsed.root).expr,
|
||||
Expr::Object { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_object_dot_field() {
|
||||
let parsed = parse_source("{ feature.enable = false; }").unwrap();
|
||||
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
|
||||
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(fields[0].path, ["feature", "enable"]);
|
||||
@@ -726,7 +747,7 @@ mod tests {
|
||||
fn parser_accepts_the_public_lossless_token_stream() {
|
||||
let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap();
|
||||
let parsed = Parser::new(tokens).parse().unwrap();
|
||||
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
|
||||
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -749,6 +770,17 @@ mod tests {
|
||||
assert!(matches!(parsed.ast.get(value).expr, Expr::Ident(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_object_rest_constraint_after_named_fields() {
|
||||
let parsed = parse_source("{ hoge = Int; fuga = String; ...Unknown }").unwrap();
|
||||
let Expr::Object { fields, rest } = &parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(fields.len(), 2);
|
||||
let rest = rest.as_ref().expect("object has a rest constraint");
|
||||
assert!(matches!(parsed.ast.get(rest.value).expr, Expr::Ident(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_as_below_composition() {
|
||||
let parsed = parse_source("value & override as Schema").unwrap();
|
||||
|
||||
@@ -11,6 +11,9 @@ pub struct ExprRef {
|
||||
pub expr: ExprId,
|
||||
}
|
||||
|
||||
/// Evaluator-owned representation of a lazy concrete value or unresolved range.
|
||||
///
|
||||
/// Embedders normally construct [`crate::Value`] and consume [`Data`] instead.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RuntimeValue {
|
||||
Concrete(ConcreteValue),
|
||||
@@ -31,6 +34,13 @@ pub enum ConcreteValue {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectValue {
|
||||
pub fields: Vec<ObjectField>,
|
||||
pub rest: Option<ObjectRest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectRest {
|
||||
pub value: ThunkId,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -67,6 +77,7 @@ pub struct ConstraintEntry {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Constraint {
|
||||
Unknown,
|
||||
Type(PrimitiveType),
|
||||
ArrayItems(ThunkId),
|
||||
MapValues(ThunkId),
|
||||
@@ -91,6 +102,7 @@ pub enum LiteralValue {
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
/// Fully materialized Decodal data.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Data {
|
||||
String(String),
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
|
||||
|
||||
use crate::{Data, HostValue};
|
||||
use crate::{Data, Value, runtime::DataField};
|
||||
|
||||
pub trait DecodalSchema {
|
||||
fn decodal_schema() -> HostValue;
|
||||
fn decodal_schema() -> Value;
|
||||
}
|
||||
|
||||
pub trait DecodalDecode: Sized {
|
||||
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
|
||||
}
|
||||
|
||||
pub trait IntoHostValue {
|
||||
fn into_host_value(self) -> HostValue;
|
||||
pub trait IntoValue {
|
||||
fn into_value(self) -> Value;
|
||||
}
|
||||
|
||||
/// A map-like receiver for fields not named by a derived struct schema.
|
||||
pub trait DecodalRest: Sized {
|
||||
fn decodal_rest_schema() -> Value;
|
||||
|
||||
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self>;
|
||||
}
|
||||
|
||||
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
|
||||
@@ -80,9 +87,38 @@ fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
|
||||
error
|
||||
}
|
||||
|
||||
impl DecodalSchema for Data {
|
||||
fn decodal_schema() -> Value {
|
||||
Value::unknown()
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalDecode for Data {
|
||||
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||
Ok(data.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Data {
|
||||
fn into_value(self) -> Value {
|
||||
match self {
|
||||
Data::String(value) => Value::string(value),
|
||||
Data::Int(value) => Value::int(value),
|
||||
Data::Float(value) => Value::float(value),
|
||||
Data::Bool(value) => Value::bool(value),
|
||||
Data::Array(items) => Value::array(items.into_iter().map(IntoValue::into_value)),
|
||||
Data::Object(fields) => Value::object(
|
||||
fields
|
||||
.into_iter()
|
||||
.map(|field| (field.name, field.value.into_value())),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalSchema for String {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::string_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::string_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +131,15 @@ impl DecodalDecode for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for String {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::string(self)
|
||||
impl IntoValue for String {
|
||||
fn into_value(self) -> Value {
|
||||
Value::string(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for &str {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::string(self)
|
||||
impl IntoValue for &str {
|
||||
fn into_value(self) -> Value {
|
||||
Value::string(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +147,8 @@ macro_rules! impl_int_decode {
|
||||
($($ty:ty),* $(,)?) => {
|
||||
$(
|
||||
impl DecodalSchema for $ty {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::int_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::int_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +162,9 @@ macro_rules! impl_int_decode {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for $ty {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::int(self as i64)
|
||||
impl IntoValue for $ty {
|
||||
fn into_value(self) -> Value {
|
||||
Value::int(self as i64)
|
||||
}
|
||||
}
|
||||
)*
|
||||
@@ -138,8 +174,8 @@ macro_rules! impl_int_decode {
|
||||
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
|
||||
|
||||
impl DecodalSchema for f64 {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::float_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::float_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,15 +189,15 @@ impl DecodalDecode for f64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for f64 {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::float(self)
|
||||
impl IntoValue for f64 {
|
||||
fn into_value(self) -> Value {
|
||||
Value::float(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalSchema for f32 {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::float_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::float_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,15 +207,15 @@ impl DecodalDecode for f32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for f32 {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::float(f64::from(self))
|
||||
impl IntoValue for f32 {
|
||||
fn into_value(self) -> Value {
|
||||
Value::float(f64::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalSchema for bool {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::bool_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::bool_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,15 +228,15 @@ impl DecodalDecode for bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for bool {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::bool(self)
|
||||
impl IntoValue for bool {
|
||||
fn into_value(self) -> Value {
|
||||
Value::bool(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema> DecodalSchema for Vec<T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::array_of(T::decodal_schema())
|
||||
fn decodal_schema() -> Value {
|
||||
Value::array_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,15 +256,15 @@ impl<T: DecodalDecode> DecodalDecode for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
|
||||
impl<T: IntoValue> IntoValue for Vec<T> {
|
||||
fn into_value(self) -> Value {
|
||||
Value::array(self.into_iter().map(IntoValue::into_value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::map_of(T::decodal_schema())
|
||||
fn decodal_schema() -> Value {
|
||||
Value::map_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,19 +284,36 @@ impl<T: DecodalDecode> DecodalDecode for BTreeMap<String, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoHostValue> IntoHostValue for BTreeMap<String, T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::object(
|
||||
impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
|
||||
fn into_value(self) -> Value {
|
||||
Value::object(
|
||||
self.into_iter()
|
||||
.map(|(name, value)| (name, value.into_host_value())),
|
||||
.map(|(name, value)| (name, value.into_value())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema + DecodalDecode> DecodalRest for BTreeMap<String, T> {
|
||||
fn decodal_rest_schema() -> Value {
|
||||
T::decodal_schema()
|
||||
}
|
||||
|
||||
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
|
||||
let Data::Object(fields) = data else {
|
||||
return Err(DecodeError::at_type("", "Object"));
|
||||
};
|
||||
fields
|
||||
.iter()
|
||||
.filter(|field| !known_fields.contains(&field.name.as_str()))
|
||||
.map(decode_rest_field::<T>)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::map_of(T::decodal_schema())
|
||||
fn decodal_schema() -> Value {
|
||||
Value::map_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,17 +335,41 @@ impl<T: DecodalDecode> DecodalDecode for std::collections::HashMap<String, T> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: IntoHostValue> IntoHostValue for std::collections::HashMap<String, T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
impl<T: IntoValue> IntoValue for std::collections::HashMap<String, T> {
|
||||
fn into_value(self) -> Value {
|
||||
let mut fields: Vec<_> = self
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.into_host_value()))
|
||||
.map(|(name, value)| (name, value.into_value()))
|
||||
.collect();
|
||||
fields.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
HostValue::object(fields)
|
||||
Value::object(fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: DecodalSchema + DecodalDecode> DecodalRest for std::collections::HashMap<String, T> {
|
||||
fn decodal_rest_schema() -> Value {
|
||||
T::decodal_schema()
|
||||
}
|
||||
|
||||
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
|
||||
let Data::Object(fields) = data else {
|
||||
return Err(DecodeError::at_type("", "Object"));
|
||||
};
|
||||
fields
|
||||
.iter()
|
||||
.filter(|field| !known_fields.contains(&field.name.as_str()))
|
||||
.map(decode_rest_field::<T>)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_rest_field<T: DecodalDecode>(field: &DataField) -> DecodeResult<(String, T)> {
|
||||
T::decodal_decode(&field.value)
|
||||
.map(|value| (field.name.clone(), value))
|
||||
.map_err(|error| prefix_error(&field.name, error))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -302,10 +379,10 @@ mod tests {
|
||||
fn string_maps_expose_map_schemas_and_decode_objects() {
|
||||
assert!(matches!(
|
||||
BTreeMap::<String, i64>::decodal_schema(),
|
||||
HostValue::MapConstraint { value, .. }
|
||||
Value::MapRange { value, .. }
|
||||
if matches!(
|
||||
*value,
|
||||
HostValue::Abstract { ref constraints, .. }
|
||||
Value::Range { ref constraints, .. }
|
||||
if constraints == &[Constraint::Type(PrimitiveType::Int)]
|
||||
)
|
||||
));
|
||||
@@ -317,4 +394,52 @@ mod tests {
|
||||
let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap();
|
||||
assert_eq!(decoded.get("api"), Some(&8080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_is_the_unknown_schema_and_round_trips_values() {
|
||||
assert!(matches!(
|
||||
Data::decodal_schema(),
|
||||
Value::Range { ref constraints, .. }
|
||||
if constraints == &[Constraint::Unknown]
|
||||
));
|
||||
|
||||
let data = Data::Object(alloc::vec![DataField {
|
||||
name: String::from("nested"),
|
||||
value: Data::Array(alloc::vec![Data::Bool(true), Data::Int(2)]),
|
||||
}]);
|
||||
assert_eq!(Data::decodal_decode(&data).unwrap(), data);
|
||||
assert!(matches!(data.into_value(), Value::Object { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_maps_decode_only_fields_outside_the_known_domain() {
|
||||
let data = Data::Object(alloc::vec![
|
||||
DataField {
|
||||
name: String::from("name"),
|
||||
value: Data::String(String::from("api")),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("priority"),
|
||||
value: Data::Int(3),
|
||||
},
|
||||
]);
|
||||
let decoded = BTreeMap::<String, Data>::decodal_decode_rest(&data, &["name"]).unwrap();
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert_eq!(decoded.get("priority"), Some(&Data::Int(3)));
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn hash_maps_can_receive_rest_fields() {
|
||||
let data = Data::Object(alloc::vec![DataField {
|
||||
name: String::from("region"),
|
||||
value: Data::String(String::from("ap-northeast-1")),
|
||||
}]);
|
||||
let decoded =
|
||||
std::collections::HashMap::<String, String>::decodal_decode_rest(&data, &[]).unwrap();
|
||||
assert_eq!(
|
||||
decoded.get("region").map(String::as_str),
|
||||
Some("ap-northeast-1")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,37 +3,46 @@ use alloc::{boxed::Box, string::String, vec::Vec};
|
||||
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
|
||||
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
|
||||
|
||||
/// A Decodal value supplied through the embedding API.
|
||||
///
|
||||
/// Unlike [`crate::Data`], a `Value` may contain unresolved ranges and
|
||||
/// defaults. The evaluator converts it into its lazy runtime representation
|
||||
/// when it is bound as a global or returned from an import loader.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum HostValue {
|
||||
pub enum Value {
|
||||
String(String),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Array(Vec<HostValue>),
|
||||
ArrayConstraint {
|
||||
item: Box<HostValue>,
|
||||
Array(Vec<Value>),
|
||||
ArrayRange {
|
||||
item: Box<Value>,
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
default: Option<Box<Value>>,
|
||||
},
|
||||
MapConstraint {
|
||||
value: Box<HostValue>,
|
||||
MapRange {
|
||||
value: Box<Value>,
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
default: Option<Box<Value>>,
|
||||
},
|
||||
Object(Vec<HostField>),
|
||||
Abstract {
|
||||
Object {
|
||||
fields: Vec<ValueField>,
|
||||
rest: Option<Box<Value>>,
|
||||
},
|
||||
Range {
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
default: Option<Box<Value>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A named field in a [`Value::Object`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HostField {
|
||||
pub struct ValueField {
|
||||
pub name: String,
|
||||
pub value: HostValue,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
impl HostValue {
|
||||
impl Value {
|
||||
pub fn string(value: impl Into<String>) -> Self {
|
||||
Self::String(value.into())
|
||||
}
|
||||
@@ -52,65 +61,105 @@ impl HostValue {
|
||||
|
||||
pub fn array<I>(items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = HostValue>,
|
||||
I: IntoIterator<Item = Value>,
|
||||
{
|
||||
Self::Array(items.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn object<I, N>(fields: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, HostValue)>,
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
Self::Object(
|
||||
fields
|
||||
Self::Object {
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| HostField {
|
||||
.map(|(name, value)| ValueField {
|
||||
name: name.into(),
|
||||
value,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
rest: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_from_paths<I, N>(fields: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, HostValue)>,
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
let mut root = Vec::new();
|
||||
for (path, value) in fields {
|
||||
insert_path(&mut root, &path.into(), value);
|
||||
}
|
||||
Self::Object(root)
|
||||
Self::Object {
|
||||
fields: root,
|
||||
rest: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_from_paths_with_rest<I, N>(fields: I, rest: Value) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
let mut root = Vec::new();
|
||||
for (path, value) in fields {
|
||||
insert_path(&mut root, &path.into(), value);
|
||||
}
|
||||
Self::Object {
|
||||
fields: root,
|
||||
rest: Some(Box::new(rest)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_with_rest<I, N>(fields: I, rest: Value) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
Self::Object {
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| ValueField {
|
||||
name: name.into(),
|
||||
value,
|
||||
})
|
||||
.collect(),
|
||||
rest: Some(Box::new(rest)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self::range_with_constraint(Constraint::Unknown)
|
||||
}
|
||||
|
||||
pub fn string_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||
}
|
||||
|
||||
pub fn int_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Int))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::Int))
|
||||
}
|
||||
|
||||
pub fn float_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Float))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::Float))
|
||||
}
|
||||
|
||||
pub fn bool_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||
}
|
||||
|
||||
pub fn array_of(item: HostValue) -> Self {
|
||||
Self::ArrayConstraint {
|
||||
pub fn array_of(item: Value) -> Self {
|
||||
Self::ArrayRange {
|
||||
item: Box::new(item),
|
||||
constraints: Vec::new(),
|
||||
default: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_of(value: HostValue) -> Self {
|
||||
Self::MapConstraint {
|
||||
pub fn map_of(value: Value) -> Self {
|
||||
Self::MapRange {
|
||||
value: Box::new(value),
|
||||
constraints: Vec::new(),
|
||||
default: None,
|
||||
@@ -118,11 +167,11 @@ impl HostValue {
|
||||
}
|
||||
|
||||
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
||||
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||
Self::range_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||
}
|
||||
|
||||
pub fn abstract_with_constraint(constraint: Constraint) -> Self {
|
||||
Self::Abstract {
|
||||
pub fn range_with_constraint(constraint: Constraint) -> Self {
|
||||
Self::Range {
|
||||
constraints: alloc::vec![constraint],
|
||||
default: None,
|
||||
}
|
||||
@@ -130,12 +179,12 @@ impl HostValue {
|
||||
|
||||
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
|
||||
match &mut self {
|
||||
Self::Abstract { constraints, .. } => constraints.push(constraint),
|
||||
Self::ArrayConstraint { constraints, .. } | Self::MapConstraint { constraints, .. } => {
|
||||
Self::Range { constraints, .. } => constraints.push(constraint),
|
||||
Self::ArrayRange { constraints, .. } | Self::MapRange { constraints, .. } => {
|
||||
constraints.push(constraint)
|
||||
}
|
||||
_ => {
|
||||
self = Self::Abstract {
|
||||
self = Self::Range {
|
||||
constraints: alloc::vec![constraint],
|
||||
default: Some(Box::new(self)),
|
||||
};
|
||||
@@ -166,53 +215,53 @@ impl HostValue {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn default(self, value: HostValue) -> Result<Self> {
|
||||
pub fn default(self, value: Value) -> Result<Self> {
|
||||
match self {
|
||||
Self::ArrayConstraint {
|
||||
Self::ArrayRange {
|
||||
item,
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::ArrayConstraint {
|
||||
} => Ok(Self::ArrayRange {
|
||||
item,
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::ArrayConstraint {
|
||||
Self::ArrayRange {
|
||||
default: Some(_), ..
|
||||
} => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
"value already has a default",
|
||||
)),
|
||||
Self::MapConstraint {
|
||||
Self::MapRange {
|
||||
value: map_value,
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::MapConstraint {
|
||||
} => Ok(Self::MapRange {
|
||||
value: map_value,
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::MapConstraint {
|
||||
Self::MapRange {
|
||||
default: Some(_), ..
|
||||
} => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
"value already has a default",
|
||||
)),
|
||||
Self::Abstract {
|
||||
Self::Range {
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::Abstract {
|
||||
} => Ok(Self::Range {
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::Abstract { .. } => Err(Diagnostic::new(
|
||||
Self::Range { .. } => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
"value already has a default",
|
||||
)),
|
||||
concrete => Ok(Self::Abstract {
|
||||
concrete => Ok(Self::Range {
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(concrete)),
|
||||
}),
|
||||
@@ -236,8 +285,8 @@ impl HostValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl HostField {
|
||||
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
|
||||
impl ValueField {
|
||||
pub fn new(name: impl Into<String>, value: Value) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
value,
|
||||
@@ -245,28 +294,40 @@ impl HostField {
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
|
||||
fn insert_path(fields: &mut Vec<ValueField>, path: &str, value: Value) {
|
||||
let mut parts = path.splitn(2, '.');
|
||||
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
if let Some(tail) = parts.next() {
|
||||
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||
if let HostValue::Object(children) = &mut field.value {
|
||||
if let Value::Object {
|
||||
fields: children, ..
|
||||
} = &mut field.value
|
||||
{
|
||||
insert_path(children, tail, value);
|
||||
} else {
|
||||
let mut children = Vec::new();
|
||||
insert_path(&mut children, tail, value);
|
||||
field.value = HostValue::Object(children);
|
||||
field.value = Value::Object {
|
||||
fields: children,
|
||||
rest: None,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let mut children = Vec::new();
|
||||
insert_path(&mut children, tail, value);
|
||||
fields.push(HostField::new(head, HostValue::Object(children)));
|
||||
fields.push(ValueField::new(
|
||||
head,
|
||||
Value::Object {
|
||||
fields: children,
|
||||
rest: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||
field.value = value;
|
||||
} else {
|
||||
fields.push(HostField::new(head, value));
|
||||
fields.push(ValueField::new(head, value));
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,38 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||
|
||||
let mut schema_fields = Vec::new();
|
||||
let mut decode_fields = Vec::new();
|
||||
let mut known_field_roots = Vec::new();
|
||||
let mut rest_field = None;
|
||||
|
||||
for field in fields {
|
||||
let ident = field.ident.expect("named field");
|
||||
let ty = field.ty;
|
||||
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
|
||||
|
||||
if attrs.rest {
|
||||
if attrs.rename_explicit || attrs.default.is_some() || !attrs.constraints.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"`rest` cannot be combined with rename, default, or field constraints",
|
||||
));
|
||||
}
|
||||
if rest_field.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"Decodal derive supports only one `rest` field",
|
||||
));
|
||||
}
|
||||
rest_field = Some((ident, ty));
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = attrs.rename.clone();
|
||||
known_field_roots.push(
|
||||
path.split('.')
|
||||
.next()
|
||||
.expect("field paths are non-empty")
|
||||
.to_owned(),
|
||||
);
|
||||
let schema_value = schema_expr(&ty, &attrs)?;
|
||||
let decode_value = decode_expr(&ty, &path, &attrs);
|
||||
|
||||
@@ -52,14 +78,40 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some((ident, ty)) = &rest_field {
|
||||
decode_fields.push(quote! {
|
||||
#ident: <#ty as ::decodal::DecodalRest>::decodal_decode_rest(
|
||||
data,
|
||||
&[#(#known_field_roots),*],
|
||||
)?
|
||||
});
|
||||
}
|
||||
|
||||
let schema_entries = if schema_fields.is_empty() {
|
||||
quote! { ::core::iter::empty::<(&'static str, ::decodal::Value)>() }
|
||||
} else {
|
||||
quote! { [#(#schema_fields),*] }
|
||||
};
|
||||
|
||||
let build_schema = if let Some((_, ty)) = &rest_field {
|
||||
quote! {
|
||||
::decodal::Value::object_from_paths_with_rest(
|
||||
#schema_entries,
|
||||
<#ty as ::decodal::DecodalRest>::decodal_rest_schema(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::decodal::Value::object_from_paths(#schema_entries)
|
||||
}
|
||||
};
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
|
||||
Ok(quote! {
|
||||
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
|
||||
fn decodal_schema() -> ::decodal::HostValue {
|
||||
::decodal::HostValue::object_from_paths([
|
||||
#(#schema_fields),*
|
||||
])
|
||||
fn decodal_schema() -> ::decodal::Value {
|
||||
#build_schema
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +128,8 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||
#[derive(Default)]
|
||||
struct FieldAttrs {
|
||||
rename: String,
|
||||
rename_explicit: bool,
|
||||
rest: bool,
|
||||
default: Option<DefaultAttr>,
|
||||
constraints: Vec<ConstraintAttr>,
|
||||
}
|
||||
@@ -95,6 +149,14 @@ impl FieldAttrs {
|
||||
if meta.path.is_ident("rename") {
|
||||
let value: LitStr = meta.value()?.parse()?;
|
||||
output.rename = value.value();
|
||||
output.rename_explicit = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("rest") {
|
||||
if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) {
|
||||
return Err(meta.error("`rest` does not accept a value"));
|
||||
}
|
||||
output.rest = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("default") {
|
||||
@@ -182,7 +244,7 @@ fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
|
||||
};
|
||||
tokens.extend(quote! {
|
||||
schema = schema
|
||||
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
|
||||
.default(::decodal::IntoValue::into_value(#default_expr))
|
||||
.expect("Decodal derive generated a valid default");
|
||||
});
|
||||
}
|
||||
@@ -210,3 +272,42 @@ fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_rest_fields() {
|
||||
let input: DeriveInput = syn::parse_quote! {
|
||||
struct Invalid {
|
||||
#[decodal(rest)]
|
||||
first: std::collections::BTreeMap<String, String>,
|
||||
#[decodal(rest)]
|
||||
second: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
expand_decodal(input)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("only one `rest` field")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_rest_field_modifiers() {
|
||||
let input: DeriveInput = syn::parse_quote! {
|
||||
struct Invalid {
|
||||
#[decodal(rest, default)]
|
||||
extra: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
expand_decodal(input)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("cannot be combined")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{Constraint, Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine, Value};
|
||||
use decodal_derive::Decodal;
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
@@ -24,6 +26,28 @@ struct Fleet {
|
||||
workers: Vec<Worker>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
struct OpenConfig {
|
||||
name: String,
|
||||
#[decodal(default = true, rename = "feature.enabled")]
|
||||
enabled: bool,
|
||||
#[decodal(rest)]
|
||||
extra: BTreeMap<String, Data>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
struct StringLabels {
|
||||
name: String,
|
||||
#[decodal(rest)]
|
||||
labels: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
struct OpenBag {
|
||||
#[decodal(rest)]
|
||||
values: BTreeMap<String, Data>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_schema_and_decode() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
@@ -148,3 +172,97 @@ fn vec_schema_applies_nested_struct_defaults() {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_map_opens_the_derived_object_and_collects_additional_fields() {
|
||||
let schema = OpenConfig::decodal_schema();
|
||||
let Value::Object {
|
||||
fields,
|
||||
rest: Some(rest),
|
||||
} = schema
|
||||
else {
|
||||
panic!("derived schema should have an object rest range")
|
||||
};
|
||||
assert_eq!(fields.len(), 2);
|
||||
assert!(matches!(
|
||||
*rest,
|
||||
Value::Range { ref constraints, .. }
|
||||
if constraints == &[Constraint::Unknown]
|
||||
));
|
||||
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global("OpenConfig", OpenConfig::decodal_schema())
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"test",
|
||||
"test",
|
||||
r#"
|
||||
{
|
||||
name = "api";
|
||||
plugin = { name = "cache"; };
|
||||
retries = 3;
|
||||
} as OpenConfig
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let config = OpenConfig::decodal_decode(&data).unwrap();
|
||||
|
||||
assert_eq!(config.name, "api");
|
||||
assert!(config.enabled);
|
||||
assert_eq!(config.extra.get("retries"), Some(&Data::Int(3)));
|
||||
assert!(matches!(config.extra.get("plugin"), Some(Data::Object(_))));
|
||||
assert!(!config.extra.contains_key("name"));
|
||||
assert!(!config.extra.contains_key("feature"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_rest_map_constrains_each_additional_field() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global("StringLabels", StringLabels::decodal_schema())
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"test",
|
||||
"test",
|
||||
r#"{ name = "service"; region = "ap-northeast-1"; tier = "edge"; } as StringLabels"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let labels = StringLabels::decodal_decode(&data).unwrap();
|
||||
assert_eq!(labels.labels.get("region"), Some(&"ap-northeast-1".into()));
|
||||
assert_eq!(labels.labels.get("tier"), Some(&"edge".into()));
|
||||
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"invalid",
|
||||
"invalid",
|
||||
r#"{ name = "service"; priority = 1; } as StringLabels"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(engine.eval_module(module).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_only_struct_collects_every_field() {
|
||||
let schema = OpenBag::decodal_schema();
|
||||
assert!(matches!(
|
||||
schema,
|
||||
Value::Object {
|
||||
ref fields,
|
||||
rest: Some(_),
|
||||
} if fields.is_empty()
|
||||
));
|
||||
|
||||
let data = Data::Object(vec![decodal::runtime::DataField {
|
||||
name: "answer".into(),
|
||||
value: Data::Int(42),
|
||||
}]);
|
||||
let bag = OpenBag::decodal_decode(&data).unwrap();
|
||||
assert_eq!(bag.values.get("answer"), Some(&Data::Int(42)));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use decodal::{
|
||||
Engine, HostEnvironment, HostValue, ImportLoader, LoadedImport, Result, RuntimeValue,
|
||||
Engine, HostEnvironment, ImportLoader, LoadedImport, Result, Value, runtime::RuntimeValue,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -116,12 +116,12 @@ pub(crate) fn complete<E: HostEnvironment>(
|
||||
let mut loader = environment.create_loader();
|
||||
let loaded = loader.load(Some(key), specifier)?;
|
||||
detail = match &loaded {
|
||||
LoadedImport::Source(source) => source.name.clone(),
|
||||
LoadedImport::Value(value) => value.key.clone(),
|
||||
LoadedImport::Source { name, .. } => name.clone(),
|
||||
LoadedImport::Value { key, .. } => key.clone(),
|
||||
};
|
||||
fields = match loaded {
|
||||
LoadedImport::Source(source) => collect_fields(&tokenize(&source.source)),
|
||||
LoadedImport::Value(value) => fields_from_host_value(&value.value),
|
||||
LoadedImport::Source { source, .. } => collect_fields(&tokenize(&source)),
|
||||
LoadedImport::Value { value, .. } => fields_from_value(&value),
|
||||
};
|
||||
} else if let Some(local) = local_fields.get(root) {
|
||||
fields = local.clone();
|
||||
@@ -229,6 +229,7 @@ fn builtin_items() -> Vec<CompletionItem> {
|
||||
("Int", CompletionKind::Type, "integer constraint", 5),
|
||||
("Float", CompletionKind::Type, "float constraint", 5),
|
||||
("Bool", CompletionKind::Type, "boolean constraint", 5),
|
||||
("Unknown", CompletionKind::Type, "unknown top range", 5),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(label, kind, detail, priority)| CompletionItem {
|
||||
@@ -267,24 +268,24 @@ fn fields_from_runtime<L: ImportLoader>(
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
fn fields_from_host_value(value: &HostValue) -> FieldTree {
|
||||
fn fields_from_value(value: &Value) -> FieldTree {
|
||||
match value {
|
||||
HostValue::Object(fields) => fields
|
||||
Value::Object { fields, .. } => fields
|
||||
.iter()
|
||||
.map(|field| (field.name.clone(), fields_from_host_value(&field.value)))
|
||||
.map(|field| (field.name.clone(), fields_from_value(&field.value)))
|
||||
.collect(),
|
||||
HostValue::ArrayConstraint {
|
||||
Value::ArrayRange {
|
||||
default: Some(value),
|
||||
..
|
||||
}
|
||||
| HostValue::MapConstraint {
|
||||
| Value::MapRange {
|
||||
default: Some(value),
|
||||
..
|
||||
}
|
||||
| HostValue::Abstract {
|
||||
| Value::Range {
|
||||
default: Some(value),
|
||||
..
|
||||
} => fields_from_host_value(value),
|
||||
} => fields_from_value(value),
|
||||
_ => FieldTree::new(),
|
||||
}
|
||||
}
|
||||
@@ -688,7 +689,7 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{
|
||||
Diagnostic, DiagnosticKind, EmptyLoader, HostValue, ImportCandidate, LoadedImport, Span,
|
||||
Diagnostic, DiagnosticKind, EmptyLoader, ImportCandidate, LoadedImport, Span, Value,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -708,15 +709,15 @@ mod tests {
|
||||
if specifier == "./post.md" {
|
||||
return Ok(LoadedImport::value(
|
||||
"post.md",
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([
|
||||
("title", HostValue::string("Hello")),
|
||||
("draft", HostValue::bool(false)),
|
||||
Value::object([
|
||||
("title", Value::string("Hello")),
|
||||
("draft", Value::bool(false)),
|
||||
]),
|
||||
),
|
||||
("body", HostValue::string("# Hello")),
|
||||
("body", Value::string("# Hello")),
|
||||
]),
|
||||
));
|
||||
}
|
||||
@@ -758,12 +759,9 @@ mod tests {
|
||||
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||
engine.bind_global(
|
||||
"App",
|
||||
HostValue::object([(
|
||||
Value::object([(
|
||||
"config",
|
||||
HostValue::object([
|
||||
("enabled", HostValue::bool_type()),
|
||||
("port", HostValue::int_type()),
|
||||
]),
|
||||
Value::object([("enabled", Value::bool_type()), ("port", Value::int_type())]),
|
||||
)]),
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -854,6 +852,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let labels = labels(result);
|
||||
assert!(labels.contains(&"String".into()));
|
||||
assert!(labels.contains(&"Unknown".into()));
|
||||
assert!(labels.contains(&"service".into()));
|
||||
assert!(labels.contains(&"value".into()));
|
||||
assert!(labels.contains(&"App".into()));
|
||||
@@ -881,6 +880,10 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(labels(result).contains(&"String".into()));
|
||||
let result = complete(&EmptyEnvironment, "main.dcdl", "Unk", 3, false)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(labels(result).contains(&"Unknown".into()));
|
||||
}
|
||||
|
||||
struct EmptyEnvironment;
|
||||
|
||||
@@ -92,7 +92,7 @@ impl SemanticAnalysis {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -100,11 +100,11 @@ mod tests {
|
||||
const ROOT: &str = r#"Post & import "./post.md""#;
|
||||
|
||||
struct ContentEnvironment {
|
||||
draft: HostValue,
|
||||
draft: Value,
|
||||
}
|
||||
|
||||
struct ContentLoader {
|
||||
draft: HostValue,
|
||||
draft: Value,
|
||||
}
|
||||
|
||||
impl ImportLoader for ContentLoader {
|
||||
@@ -122,15 +122,15 @@ mod tests {
|
||||
}
|
||||
Ok(LoadedImport::value(
|
||||
"content/post.md",
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([
|
||||
("title", HostValue::string("Hello")),
|
||||
Value::object([
|
||||
("title", Value::string("Hello")),
|
||||
("draft", self.draft.clone()),
|
||||
]),
|
||||
),
|
||||
("body", HostValue::string("# Hello")),
|
||||
("body", Value::string("# Hello")),
|
||||
]),
|
||||
))
|
||||
}
|
||||
@@ -148,15 +148,15 @@ mod tests {
|
||||
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||
engine.bind_global(
|
||||
"Post",
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([
|
||||
("title", HostValue::string_type()),
|
||||
("draft", HostValue::bool_type()),
|
||||
Value::object([
|
||||
("title", Value::string_type()),
|
||||
("draft", Value::bool_type()),
|
||||
]),
|
||||
),
|
||||
("body", HostValue::string_type()),
|
||||
("body", Value::string_type()),
|
||||
]),
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
#[test]
|
||||
fn injected_environment_matches_direct_evaluation() {
|
||||
let environment = ContentEnvironment {
|
||||
draft: HostValue::bool(false),
|
||||
draft: Value::bool(false),
|
||||
};
|
||||
let direct = evaluate_direct(&environment).unwrap();
|
||||
let service = LanguageService::new(&environment);
|
||||
@@ -186,7 +186,7 @@ mod tests {
|
||||
#[test]
|
||||
fn injected_environment_preserves_import_diagnostics() {
|
||||
let service = LanguageService::new(ContentEnvironment {
|
||||
draft: HostValue::string("maybe"),
|
||||
draft: Value::string("maybe"),
|
||||
});
|
||||
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use decodal::{
|
||||
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Param, SourceForm, Span, SyntaxToken,
|
||||
SyntaxTokenKind,
|
||||
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, ObjectRest, Param, SourceForm, Span,
|
||||
SyntaxToken, SyntaxTokenKind,
|
||||
ast::{MatchArm, UnaryOp},
|
||||
parse_source, tokenize_source,
|
||||
};
|
||||
@@ -81,7 +81,7 @@ impl<'a> Formatter<'a> {
|
||||
fn format(&self, root: ExprId, source_form: SourceForm) -> String {
|
||||
let mut out = String::new();
|
||||
if source_form == SourceForm::Fields {
|
||||
let Expr::Object(fields) = &self.ast.get(root).expr else {
|
||||
let Expr::Object { fields, .. } = &self.ast.get(root).expr else {
|
||||
unreachable!("field-form sources parse to an object")
|
||||
};
|
||||
self.write_field_list(&mut out, fields, 0, self.source.len(), 0);
|
||||
@@ -145,7 +145,7 @@ impl<'a> Formatter<'a> {
|
||||
if self.has_comment(node.span)
|
||||
&& !matches!(
|
||||
node.expr,
|
||||
Expr::Object(_) | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
|
||||
Expr::Object { .. } | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
|
||||
)
|
||||
{
|
||||
out.push_str(self.raw(node.span));
|
||||
@@ -162,7 +162,9 @@ impl<'a> Formatter<'a> {
|
||||
Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => {
|
||||
out.push_str(self.raw(node.span));
|
||||
}
|
||||
Expr::Object(fields) => self.write_object(out, node.span, fields, indent),
|
||||
Expr::Object { fields, rest } => {
|
||||
self.write_object(out, node.span, fields, rest.as_ref(), indent)
|
||||
}
|
||||
Expr::Array(items) => self.write_array(out, node.span, items, indent),
|
||||
Expr::ArrayConstraint { item } => {
|
||||
out.push_str("[...");
|
||||
@@ -241,15 +243,50 @@ impl<'a> Formatter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_object(&self, out: &mut String, span: Span, fields: &[Field], indent: usize) {
|
||||
fn write_object(
|
||||
&self,
|
||||
out: &mut String,
|
||||
span: Span,
|
||||
fields: &[Field],
|
||||
rest: Option<&ObjectRest>,
|
||||
indent: usize,
|
||||
) {
|
||||
let (start, end) =
|
||||
self.delimited_range(span, SyntaxTokenKind::LBrace, SyntaxTokenKind::RBrace);
|
||||
if fields.is_empty() && !self.has_comment_between(start, end) {
|
||||
if fields.is_empty() && rest.is_none() && !self.has_comment_between(start, end) {
|
||||
out.push_str("{}");
|
||||
return;
|
||||
}
|
||||
out.push_str("{\n");
|
||||
self.write_field_list(out, fields, start, end, indent + INDENT);
|
||||
let field_end = rest.map_or(end, |rest| rest.span.start as usize);
|
||||
self.write_field_list(out, fields, start, field_end, indent + INDENT);
|
||||
if let Some(rest) = rest {
|
||||
write_indent(out, indent + INDENT);
|
||||
out.push_str("...");
|
||||
let value_start = self.ast.span(rest.value).start as usize;
|
||||
let ellipsis = self
|
||||
.tokens_between(rest.span.start as usize, value_start)
|
||||
.find(|token| token.kind == SyntaxTokenKind::Ellipsis)
|
||||
.expect("parsed object rest constraints contain `...`");
|
||||
if self.has_comment_between(ellipsis.span.end as usize, value_start) {
|
||||
self.write_between(
|
||||
out,
|
||||
ellipsis.span.end as usize,
|
||||
value_start,
|
||||
Some(ellipsis.span.end as usize),
|
||||
indent + INDENT,
|
||||
);
|
||||
write_indent(out, indent + INDENT);
|
||||
}
|
||||
self.write_expr(out, rest.value, indent + INDENT, 0);
|
||||
self.write_between(
|
||||
out,
|
||||
rest.span.end as usize,
|
||||
end,
|
||||
Some(rest.span.end as usize),
|
||||
indent + INDENT,
|
||||
);
|
||||
}
|
||||
write_indent(out, indent);
|
||||
out.push('}');
|
||||
}
|
||||
@@ -524,7 +561,7 @@ impl<'a> Formatter<'a> {
|
||||
Expr::As { narrower, wider } => {
|
||||
self.is_inline_expr(*narrower) && self.is_inline_expr(*wider)
|
||||
}
|
||||
Expr::Object(_) | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
|
||||
Expr::Object { .. } | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -707,6 +744,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_unknown_and_object_rest_constraints() {
|
||||
let source =
|
||||
"value={hoge=Int;fuga=String;# remaining fields\n...# unconstrained\nUnknown};";
|
||||
let formatted = format_source(source).unwrap();
|
||||
assert_eq!(
|
||||
formatted,
|
||||
"value = {\n hoge = Int;\n fuga = String; # remaining fields\n ... # unconstrained\n Unknown\n};\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_regex_and_escaped_strings() {
|
||||
let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#;
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
use decodal::{
|
||||
Diagnostic as DecodalDiagnostic, DiagnosticKind, HostEnvironment, ImportCandidate,
|
||||
ImportLoader, LoadedImport, LoadedSource, SourceId, Span,
|
||||
ImportLoader, LoadedImport, SourceId, Span,
|
||||
};
|
||||
use decodal_language_service::{
|
||||
CompletionKind as ServiceCompletionKind, CompletionResult, LanguageService, SemanticAnalysis,
|
||||
@@ -667,11 +667,11 @@ impl ImportLoader for FileSystemLoader {
|
||||
)
|
||||
})?
|
||||
};
|
||||
Ok(LoadedImport::Source(LoadedSource {
|
||||
Ok(LoadedImport::Source {
|
||||
key: key.clone(),
|
||||
name: key,
|
||||
source,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn complete_import(
|
||||
@@ -743,7 +743,7 @@ mod tests {
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use decodal::{Engine, HostValue};
|
||||
use decodal::{Engine, Value};
|
||||
use lsp_server::RequestId;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -772,12 +772,12 @@ mod tests {
|
||||
.is_some_and(|source| source == "draft: false");
|
||||
Ok(LoadedImport::value(
|
||||
"content/post.md",
|
||||
HostValue::object([(
|
||||
Value::object([(
|
||||
"draft",
|
||||
if draft {
|
||||
HostValue::bool(false)
|
||||
Value::bool(false)
|
||||
} else {
|
||||
HostValue::string("maybe")
|
||||
Value::string("maybe")
|
||||
},
|
||||
)]),
|
||||
))
|
||||
@@ -794,10 +794,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||
engine.bind_global(
|
||||
"Post",
|
||||
HostValue::object([("draft", HostValue::bool_type())]),
|
||||
)?;
|
||||
engine.bind_global("Post", Value::object([("draft", Value::bool_type())]))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use decodal_language_service::{CompletionKind, LanguageService};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod host_value;
|
||||
mod value;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web_environment;
|
||||
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
use decodal::{CompareOp, Constraint, HostValue, LiteralValue, PrimitiveType};
|
||||
use serde_json::{Map, Value};
|
||||
use decodal::{CompareOp, Constraint, LiteralValue, PrimitiveType, Value};
|
||||
use serde_json::{Map, Value as JsonValue};
|
||||
|
||||
pub(crate) fn from_json(value: &Value) -> Result<HostValue, String> {
|
||||
pub(crate) fn from_json(value: &JsonValue) -> Result<Value, String> {
|
||||
match value {
|
||||
Value::Null => Err(String::from("null is not a Decodal host value")),
|
||||
Value::Bool(value) => Ok(HostValue::bool(*value)),
|
||||
Value::Number(value) => number(value),
|
||||
Value::String(value) => Ok(HostValue::string(value)),
|
||||
Value::Array(items) => items
|
||||
JsonValue::Null => Err(String::from("null is not a Decodal value")),
|
||||
JsonValue::Bool(value) => Ok(Value::bool(*value)),
|
||||
JsonValue::Number(value) => number(value),
|
||||
JsonValue::String(value) => Ok(Value::string(value)),
|
||||
JsonValue::Array(items) => items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, value)| {
|
||||
from_json(value).map_err(|error| format!("array item {index}: {error}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(HostValue::array),
|
||||
Value::Object(fields) => object(fields),
|
||||
.map(Value::array),
|
||||
JsonValue::Object(fields) => object(fields),
|
||||
}
|
||||
}
|
||||
|
||||
fn number(value: &serde_json::Number) -> Result<HostValue, String> {
|
||||
fn number(value: &serde_json::Number) -> Result<Value, String> {
|
||||
if let Some(value) = value.as_i64() {
|
||||
return Ok(HostValue::int(value));
|
||||
return Ok(Value::int(value));
|
||||
}
|
||||
if let Some(value) = value.as_u64() {
|
||||
return i64::try_from(value)
|
||||
.map(HostValue::int)
|
||||
.map_err(|_| String::from("integer host value is outside the signed 64-bit range"));
|
||||
.map(Value::int)
|
||||
.map_err(|_| String::from("integer value is outside the signed 64-bit range"));
|
||||
}
|
||||
value
|
||||
.as_f64()
|
||||
.filter(|value| value.is_finite())
|
||||
.map(HostValue::float)
|
||||
.ok_or_else(|| String::from("invalid numeric host value"))
|
||||
.map(Value::float)
|
||||
.ok_or_else(|| String::from("invalid numeric value"))
|
||||
}
|
||||
|
||||
fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
fn object(fields: &Map<String, JsonValue>) -> Result<Value, String> {
|
||||
let Some(descriptor) = fields.get("$decodal") else {
|
||||
return fields
|
||||
.iter()
|
||||
@@ -45,7 +45,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
.map_err(|error| format!("field `{name}`: {error}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(HostValue::object);
|
||||
.map(Value::object);
|
||||
};
|
||||
|
||||
let descriptor = descriptor
|
||||
@@ -59,6 +59,14 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
.map(Box::new);
|
||||
|
||||
match descriptor {
|
||||
"Unknown" => {
|
||||
let mut all_constraints = vec![Constraint::Unknown];
|
||||
all_constraints.extend(constraints);
|
||||
Ok(Value::Range {
|
||||
constraints: all_constraints,
|
||||
default,
|
||||
})
|
||||
}
|
||||
"String" | "Int" | "Float" | "Bool" => {
|
||||
let primitive = match descriptor {
|
||||
"String" => PrimitiveType::String,
|
||||
@@ -69,7 +77,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
};
|
||||
let mut all_constraints = vec![Constraint::Type(primitive)];
|
||||
all_constraints.extend(constraints);
|
||||
Ok(HostValue::Abstract {
|
||||
Ok(Value::Range {
|
||||
constraints: all_constraints,
|
||||
default,
|
||||
})
|
||||
@@ -78,7 +86,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
let item = fields
|
||||
.get("item")
|
||||
.ok_or_else(|| String::from("Array descriptor requires `item`"))?;
|
||||
Ok(HostValue::ArrayConstraint {
|
||||
Ok(Value::ArrayRange {
|
||||
item: Box::new(from_json(item)?),
|
||||
constraints,
|
||||
default,
|
||||
@@ -88,21 +96,49 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
let value = fields
|
||||
.get("value")
|
||||
.ok_or_else(|| String::from("Map descriptor requires `value`"))?;
|
||||
Ok(HostValue::MapConstraint {
|
||||
Ok(Value::MapRange {
|
||||
value: Box::new(from_json(value)?),
|
||||
constraints,
|
||||
default,
|
||||
})
|
||||
}
|
||||
"Abstract" => Ok(HostValue::Abstract {
|
||||
"Object" => {
|
||||
if !constraints.is_empty() {
|
||||
return Err(String::from(
|
||||
"Object descriptor does not accept `constraints`; constrain its fields or rest range",
|
||||
));
|
||||
}
|
||||
if default.is_some() {
|
||||
return Err(String::from(
|
||||
"Object descriptor does not accept `default`; place defaults on its fields",
|
||||
));
|
||||
}
|
||||
let object_fields = fields
|
||||
.get("fields")
|
||||
.and_then(JsonValue::as_object)
|
||||
.ok_or_else(|| String::from("Object descriptor requires object `fields`"))?;
|
||||
let value_fields = object_fields
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
from_json(value)
|
||||
.map(|value| (name.clone(), value))
|
||||
.map_err(|error| format!("field `{name}`: {error}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let rest = fields
|
||||
.get("rest")
|
||||
.ok_or_else(|| String::from("Object descriptor requires `rest`"))?;
|
||||
Ok(Value::object_with_rest(value_fields, from_json(rest)?))
|
||||
}
|
||||
"Range" => Ok(Value::Range {
|
||||
constraints,
|
||||
default,
|
||||
}),
|
||||
name => Err(format!("unknown Decodal host descriptor `{name}`")),
|
||||
name => Err(format!("unknown Decodal value descriptor `{name}`")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
|
||||
fn parse_constraints(value: Option<&JsonValue>) -> Result<Vec<Constraint>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
@@ -118,12 +154,13 @@ fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_constraint(value: &Value) -> Result<Constraint, String> {
|
||||
fn parse_constraint(value: &JsonValue) -> Result<Constraint, String> {
|
||||
let fields = value
|
||||
.as_object()
|
||||
.ok_or_else(|| String::from("constraint must be an object"))?;
|
||||
let kind = required_string(fields, "kind")?;
|
||||
match kind {
|
||||
"unknown" => Ok(Constraint::Unknown),
|
||||
"type" => match required_string(fields, "value")? {
|
||||
"String" => Ok(Constraint::Type(PrimitiveType::String)),
|
||||
"Int" => Ok(Constraint::Type(PrimitiveType::Int)),
|
||||
@@ -154,29 +191,29 @@ fn parse_constraint(value: &Value) -> Result<Constraint, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(value: &Value) -> Result<LiteralValue, String> {
|
||||
fn literal(value: &JsonValue) -> Result<LiteralValue, String> {
|
||||
match value {
|
||||
Value::String(value) => Ok(LiteralValue::String(value.clone())),
|
||||
Value::Bool(value) => Ok(LiteralValue::Bool(*value)),
|
||||
Value::Number(value) => match number(value)? {
|
||||
HostValue::Int(value) => Ok(LiteralValue::Int(value)),
|
||||
HostValue::Float(value) => Ok(LiteralValue::Float(value)),
|
||||
JsonValue::String(value) => Ok(LiteralValue::String(value.clone())),
|
||||
JsonValue::Bool(value) => Ok(LiteralValue::Bool(*value)),
|
||||
JsonValue::Number(value) => match number(value)? {
|
||||
Value::Int(value) => Ok(LiteralValue::Int(value)),
|
||||
Value::Float(value) => Ok(LiteralValue::Float(value)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => Err(String::from("comparison value must be a primitive literal")),
|
||||
}
|
||||
}
|
||||
|
||||
fn required_string<'a>(fields: &'a Map<String, Value>, name: &str) -> Result<&'a str, String> {
|
||||
fn required_string<'a>(fields: &'a Map<String, JsonValue>, name: &str) -> Result<&'a str, String> {
|
||||
fields
|
||||
.get(name)
|
||||
.and_then(Value::as_str)
|
||||
.and_then(JsonValue::as_str)
|
||||
.ok_or_else(|| format!("constraint requires string `{name}`"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use decodal::{Constraint, HostValue, LiteralValue, PrimitiveType};
|
||||
use decodal::{Constraint, LiteralValue, PrimitiveType, Value};
|
||||
|
||||
use super::from_json;
|
||||
|
||||
@@ -188,11 +225,11 @@ mod tests {
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::object([
|
||||
("body", HostValue::string("# Hello")),
|
||||
Value::object([
|
||||
("body", Value::string("# Hello")),
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([("draft", HostValue::bool(false))]),
|
||||
Value::object([("draft", Value::bool(false))]),
|
||||
),
|
||||
])
|
||||
);
|
||||
@@ -210,8 +247,8 @@ mod tests {
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::ArrayConstraint {
|
||||
item: Box::new(HostValue::Abstract {
|
||||
Value::ArrayRange {
|
||||
item: Box::new(Value::Range {
|
||||
constraints: vec![
|
||||
Constraint::Type(PrimitiveType::Int),
|
||||
Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)),
|
||||
@@ -219,10 +256,7 @@ mod tests {
|
||||
default: None,
|
||||
}),
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(HostValue::array([
|
||||
HostValue::int(1),
|
||||
HostValue::int(2),
|
||||
]))),
|
||||
default: Some(Box::new(Value::array([Value::int(1), Value::int(2),]))),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -236,10 +270,41 @@ mod tests {
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::MapConstraint {
|
||||
value: Box::new(HostValue::bool_type()),
|
||||
Value::MapRange {
|
||||
value: Box::new(Value::bool_type()),
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(HostValue::object([] as [(&str, HostValue); 0]))),
|
||||
default: Some(Box::new(Value::object([] as [(&str, Value); 0]))),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_unknown_and_open_object_descriptors() {
|
||||
let value = serde_json::json!({
|
||||
"$decodal": "Object",
|
||||
"fields": {
|
||||
"name": { "$decodal": "String" },
|
||||
},
|
||||
"rest": { "$decodal": "Unknown" },
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
Value::object_with_rest([("name", Value::string_type())], Value::unknown(),)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_general_range_descriptors() {
|
||||
let value = serde_json::json!({
|
||||
"$decodal": "Range",
|
||||
"constraints": [{ "kind": "predicate", "value": "available" }],
|
||||
"default": true,
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
Value::Range {
|
||||
constraints: vec![Constraint::BuiltinPredicate("available".into())],
|
||||
default: Some(Box::new(Value::bool(true))),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{
|
||||
Diagnostic, DiagnosticKind, Engine, HostEnvironment, HostValue, ImportCandidate, ImportLoader,
|
||||
LoadedImport, Span,
|
||||
Diagnostic, DiagnosticKind, Engine, HostEnvironment, ImportCandidate, ImportLoader,
|
||||
LoadedImport, Span, Value as DecodalValue,
|
||||
};
|
||||
use js_sys::{Function, Reflect};
|
||||
use serde_json::Value;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
use crate::host_value;
|
||||
use crate::value;
|
||||
|
||||
pub(crate) struct JsEnvironment {
|
||||
globals: BTreeMap<String, HostValue>,
|
||||
globals: BTreeMap<String, DecodalValue>,
|
||||
load_import: Option<Function>,
|
||||
complete_import: Option<Function>,
|
||||
}
|
||||
@@ -37,7 +37,7 @@ impl JsEnvironment {
|
||||
globals
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
host_value::from_json(value)
|
||||
value::from_json(value)
|
||||
.map(|value| (name.clone(), value))
|
||||
.map_err(|error| {
|
||||
JsValue::from_str(&format!("invalid global `{name}`: {error}"))
|
||||
@@ -133,7 +133,7 @@ fn loaded_import(value: &Value) -> Result<LoadedImport, String> {
|
||||
let value = fields
|
||||
.get("value")
|
||||
.ok_or_else(|| String::from("value import requires `value`"))?;
|
||||
let value = host_value::from_json(value)
|
||||
let value = value::from_json(value)
|
||||
.map_err(|error| format!("invalid imported value: {error}"))?;
|
||||
Ok(LoadedImport::value(key, value))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user