Add unknown ranges and open object values
This commit is contained in:
@@ -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