///|
fn item_type_name(node : @xml.Node) -> String {
if node.attr("typeRef") != "" {
node.attr("typeRef")
} else {
node.content("typeRef")
}
}
///|
fn join_specs(fields : Array[String]) -> String {
let out = StringBuilder()
for i, field in fields {
if i > 0 {
out.write_string(", ")
}
out.write_string(field)
}
out.to_string()
}
///|
/// Structural descriptor of an ItemDefinition, used for `instance of` checks.
fn definition_descriptor(
model : Model,
node : @xml.Node,
depth : Int32,
) -> String {
if depth > 32 {
return "Any"
}
let components = node.all("itemComponent")
let base = if !components.is_empty() {
let fields : Array[String] = []
for component in components {
fields.push(
component.attr("name") +
": " +
type_descriptor(model, item_type_name(component), depth + 1),
)
}
"context<" + join_specs(fields) + ">"
} else {
type_descriptor(model, item_type_name(node), depth + 1)
}
if node.attr("isCollection") == "true" {
"list<" + base + ">"
} else {
base
}
}
///|
fn type_descriptor(model : Model, type_name : String, depth : Int32) -> String {
if depth > 32 {
return "Any"
}
let name = type_name.trim().to_owned()
if name == "" || name == "Any" || name == "any" {
return "any"
}
for definition in model.root.all("itemDefinition") {
if definition.attr("name") == name {
return definition_descriptor(model, definition, depth + 1)
}
}
name
}
///|
/// Registry of named ItemDefinitions exposed to FEEL as `instance of` types.
fn type_registry(model : Model) -> Map[String, @types.Value] {
let out : Map[String, @types.Value] = Map([])
for definition in model.root.all("itemDefinition") {
out[definition.attr("name")] = Text(
definition_descriptor(model, definition, 0),
)
}
out
}
///|
/// Validate supported primitive, collection, alias and structured ItemDefinitions.
/// No implicit coercions are performed in this version.
fn validate_type(
model : Model,
type_name : String,
value : @types.Value,
depth : Int32,
) -> Unit raise @types.EngineError {
if depth > 32 {
raise @types.fail("E_TYPE_CYCLE", "recursive ItemDefinition")
}
if value is Null {
return
}
match type_name {
"" | "Any" | "any" => return
"number" => if value is Number(_) { return }
"string" => if value is Text(_) { return }
"boolean" => if value is Boolean(_) { return }
"date" => if value is Date(_) { return }
"time" => if value is Time(_) { return }
"date and time" | "dateTime" => if value is DateTime(_) { return }
"days and time duration" | "dayTimeDuration" =>
if value is DaysSeconds(_) {
return
}
"years and months duration" | "yearMonthDuration" =>
if value is YearsMonths(_) {
return
}
"duration" =>
if value is DaysSeconds(_) || value is YearsMonths(_) {
return
}
"list" => if value is List(_) { return }
"context" => if value is Context(_) { return }
"range" => if value is Range(_) { return }
"function" => if value is Function(_) { return }
_ => {
for definition in model.root.all("itemDefinition") {
if definition.attr("name") == type_name {
validate_definition(model, definition, value, depth + 1, true)
return
}
}
raise @types.fail(
"E_TYPE_UNSUPPORTED",
"unknown or unsupported type: " + type_name,
)
}
}
raise @types.fail("E_TYPE", "value does not conform to " + type_name)
}
///|
fn validate_definition(
model : Model,
node : @xml.Node,
value : @types.Value,
depth : Int32,
collection : Bool,
) -> Unit raise @types.EngineError {
if depth > 32 {
raise @types.fail("E_TYPE_CYCLE", "recursive ItemDefinition")
}
if collection && node.attr("isCollection") == "true" {
match value {
List(xs) =>
for x in xs {
validate_definition(model, node, x, depth + 1, false)
}
_ =>
raise @types.fail(
"E_TYPE_COLLECTION", "expected list; singleton coercion unsupported",
)
}
return
}
let components = node.all("itemComponent")
if !components.is_empty() {
match value {
Context(ctx) =>
for component in components {
let v = match ctx.get(component.attr("name")) {
Some(v) => v
None =>
raise @types.fail(
"E_TYPE_FIELD",
"missing field: " + component.attr("name"),
)
}
validate_definition(model, component, v, depth + 1, true)
}
_ => raise @types.fail("E_TYPE", "expected context")
}
} else {
let type_name = if node.attr("typeRef") != "" {
node.attr("typeRef")
} else {
node.content("typeRef")
}
validate_type(model, type_name, value, depth + 1)
}
if node.child("allowedValues") is Some(allowed) {
if !@eval.unary_test(allowed.content("text"), value, Map([])) {
raise @types.fail(
"E_ALLOWED_VALUES", "ItemDefinition allowedValues violation",
)
}
}
}
///|
fn validate_variable(
model : Model,
node : @xml.Node,
value : @types.Value,
) -> Unit raise @types.EngineError {
if node.child("variable") is Some(variable) {
validate_type(model, variable.attr("typeRef"), value, 0)
}
}
///|
/// Apply FEEL singleton adaptation before checking the declared type.
fn coerce_type(
model : Model,
name : String,
value : @types.Value,
depth : Int32,
) -> @types.Value raise @types.EngineError {
if depth > 32 {
raise @types.fail("E_TYPE_CYCLE", "recursive ItemDefinition")
}
if value is Null || name == "" || name == "Any" || name == "any" {
return value
}
for definition in model.root.all("itemDefinition") {
if definition.attr("name") == name {
return coerce_definition(model, definition, value, depth + 1, true)
}
}
let value = if name != "list" {
match value {
List([one]) => one
_ => value
}
} else {
value
}
let value = if (name == "date and time" || name == "dateTime") &&
value is Date(d) {
@types.DateTime({
date: d,
time: @types.make_time(0, 0, 0, 0, @types.Fixed(0)),
})
} else {
value
}
validate_type(model, name, value, depth)
value
}
///|
fn coerce_definition(
model : Model,
node : @xml.Node,
value : @types.Value,
depth : Int32,
collection : Bool,
) -> @types.Value raise @types.EngineError {
if depth > 32 {
raise @types.fail("E_TYPE_CYCLE", "recursive ItemDefinition")
}
if value is Null {
return value
}
if collection && node.attr("isCollection") == "true" {
let xs = match value {
List(xs) => xs
_ => [value]
}
return List(
xs.map(x => coerce_definition(model, node, x, depth + 1, false)),
)
}
if node.child("functionItem") is Some(item) {
if value is Function(f) {
return Function({
name: f.name,
params: f.params,
callback: args => {
coerce_type(
model,
item.attr("outputTypeRef"),
(f.callback)(args),
depth + 1,
)
},
})
}
raise @types.fail("E_TYPE", "expected function")
}
let components = node.all("itemComponent")
let value = if !components.is_empty() {
match value {
List([one]) => one
_ => value
}
} else {
value
}
let result = if components.is_empty() {
coerce_type(model, item_type_name(node), value, depth + 1)
} else {
let ctx = match value {
Context(c) => c.copy()
_ => raise @types.fail("E_TYPE", "expected context")
}
for component in components {
let key = component.attr("name")
ctx[key] = coerce_definition(
model,
component,
match ctx.get(key) {
Some(v) => v
None => raise @types.fail("E_TYPE", "missing field: " + key)
},
depth + 1,
true,
)
}
@types.Context(ctx)
}
if node.child("allowedValues") is Some(allowed) {
if !@eval.unary_test(allowed.content("text"), result, Map([])) {
raise @types.fail(
"E_ALLOWED_VALUES", "ItemDefinition allowedValues violation",
)
}
}
result
}
///|
fn coerce_variable(
model : Model,
node : @xml.Node,
value : @types.Value,
) -> @types.Value raise @types.EngineError {
match node.child("variable") {
Some(v) => coerce_type(model, v.attr("typeRef"), value, 0)
None => value
}
}
///|
/// Decode exact JSON input and convert declared temporal fields recursively.
/// Conversion is confined to this JSON boundary, not FEEL string coercion.
pub fn json_inputs(
model : Model,
json : String,
) -> Map[String, @types.Value] raise @types.EngineError {
let out = @types.json_context(json)
for node in model.root.children {
if node.uri != model.root.uri ||
(node.name != "inputData" && node.name != "decision") {
continue
}
let name = node.attr("name")
if out.get(name) is Some(value) && node.child("variable") is Some(variable) {
out[name] = decode_declared(model, variable, value, 0, true)
}
}
out
}
///|
fn decode_declared(
model : Model,
node : @xml.Node,
value : @types.Value,
depth : Int32,
collection : Bool,
) -> @types.Value raise @types.EngineError {
if depth > 32 {
raise @types.fail("E_TYPE_CYCLE", "recursive JSON ItemDefinition")
}
if value is Null {
return value
}
if collection && node.attr("isCollection") == "true" {
let items = match value {
List(xs) => xs
_ => [value]
}
return List(
items.map(x => decode_declared(model, node, x, depth + 1, false)),
)
}
let fields = node.all("itemComponent")
if !fields.is_empty() {
let values = match value {
Context(c) => c.copy()
_ => raise @types.fail("E_TYPE", "JSON field must be an object")
}
for field in fields {
let key = field.attr("name")
if values.get(key) is Some(v) {
values[key] = decode_declared(model, field, v, depth + 1, true)
}
}
return Context(values)
}
let name = item_type_name(node)
for definition in model.root.all("itemDefinition") {
if definition.attr("name") == name {
return decode_declared(model, definition, value, depth + 1, true)
}
}
if value is List(items) {
return List(
items.map(x => decode_declared(model, node, x, depth + 1, false)),
)
}
if value is Text(text) {
match name {
"date" => return Date(@types.parse_date(text))
"time" => return Time(@types.parse_time(text))
"dateTime" | "date and time" =>
return DateTime(@types.parse_datetime(text))
"dayTimeDuration"
| "days and time duration"
| "yearMonthDuration"
| "years and months duration"
| "duration" => {
let (days, months) = @types.parse_duration(text)
let ym = text.contains("Y") ||
text.split("T").to_array()[0].contains("M")
if name == "dayTimeDuration" || name == "days and time duration" {
if ym {
raise @types.fail("E_TYPE", "expected days and time duration")
}
return DaysSeconds(days)
}
if name == "yearMonthDuration" || name == "years and months duration" {
if !ym {
raise @types.fail("E_TYPE", "expected years and months duration")
}
return YearsMonths(months)
}
return if ym { YearsMonths(months) } else { DaysSeconds(days) }
}
_ => ()
}
}
value
}