Add unknown ranges and open object values

This commit is contained in:
2026-08-14 08:00:22 +09:00
parent 8c50dd202d
commit 848c7f169f
53 changed files with 9047 additions and 6572 deletions
+1 -1
View File
@@ -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))),
}
);
}
+6 -6
View File
@@ -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))
}