///|
/// Ordered JSON value. Object keys stay in source order; `Json` objects do not.
priv enum J {
Null
Bool(Bool)
Num(String)
Str(String)
Arr(Array[J])
Obj(Array[(String, J)])
}
///|
/// Type information kept for one component or call parameter.
pub enum SchemaShape {
Any
Str
Num
Boolean
Obj
Arr
Null
Ref(String)
Enum(Array[String])
Union(Array[SchemaShape])
Unknown(String)
}
///|
pub struct ParamDef {
name : String
required : Bool
shape : SchemaShape
}
///|
pub struct ComponentDef {
name : String
params : Array[ParamDef]
}
///|
pub enum CallKind {
Function
Action
}
///|
pub struct CallDef {
name : String
kind : CallKind
params : Array[ParamDef]
}
///|
pub struct SchemaError {
path : String
kind : SchemaKind
}
///|
pub fn SchemaError::message(self : SchemaError) -> String {
self.kind.message()
}
///|
/// Component and call tables compiled from a library JSON Schema document.
pub struct SchemaCatalog {
components : Array[ComponentDef]
calls : Array[CallDef]
errors : Array[SchemaError]
}
///|
priv struct SchemaReader {
src : String
mut index : Int32
mut failed : Bool
mut kind : SchemaKind
}
///|
fn SchemaReader::ended(self : SchemaReader) -> Bool {
self.index >= self.src.length()
}
///|
fn SchemaReader::peek(self : SchemaReader) -> Int32 {
if self.ended() {
-1
} else {
self.src.at(self.index).to_int()
}
}
///|
fn SchemaReader::peek_at(self : SchemaReader, offset : Int32) -> Int32 {
let index = self.index + offset
if index >= self.src.length() {
-1
} else {
self.src.at(index).to_int()
}
}
///|
fn SchemaReader::advance(self : SchemaReader) -> Unit {
if !self.ended() {
self.index += 1
}
}
///|
fn SchemaReader::fail(self : SchemaReader, kind : SchemaKind) -> Unit {
if !self.failed {
self.failed = true
self.kind = kind
}
}
///|
fn SchemaReader::skip_ws(self : SchemaReader) -> Unit {
while !self.ended() {
let current = self.peek()
if current == code(' ') ||
current == code('\t') ||
current == code('\n') ||
current == code('\r') {
self.advance()
} else {
break
}
}
}
///|
fn SchemaReader::consume(self : SchemaReader, text : String) -> Bool {
let mut offset = 0
while offset < text.length() {
if self.peek_at(offset) != text.at(offset).to_int() {
return false
}
offset += 1
}
let mut left = text.length()
while left > 0 {
self.advance()
left -= 1
}
true
}
///|
fn SchemaReader::parse_string(self : SchemaReader) -> String {
self.advance()
let buf = StringBuilder()
while !self.ended() && !self.failed {
let current = self.peek()
if current == code('"') {
self.advance()
return buf.to_string()
}
if current == code('\\') {
self.advance()
self.parse_escape(buf)
} else if current < 0x20 {
self.fail(InvalidJsonString(self.index))
return ""
} else {
match current.to_char() {
Some(ch) => buf.write_char(ch)
None => self.fail(InvalidJsonString(self.index))
}
self.advance()
}
}
self.fail(InvalidJsonString(self.index))
""
}
///|
fn SchemaReader::parse_escape(self : SchemaReader, buf : StringBuilder) -> Unit {
let current = self.peek()
self.advance()
match current.to_char() {
Some('u') =>
match self.read_hex4() {
Some(point) =>
if point >= 0xD800 && point <= 0xDFFF {
self.fail(InvalidJsonString(self.index))
} else {
write_code_point(buf, point)
}
None => self.fail(InvalidJsonString(self.index))
}
Some(ch) =>
if !write_schema_escape(buf, ch) {
self.fail(InvalidJsonString(self.index))
}
None => self.fail(InvalidJsonString(self.index))
}
}
///|
fn SchemaReader::read_hex4(self : SchemaReader) -> Int32? {
if self.index + 4 > self.src.length() {
return None
}
let mut value = 0
for offset in 0..<4 {
match hex_digit(self.peek_at(offset)) {
Some(digit) => value = value * 16 + digit
None => return None
}
}
for _offset in 0..<4 {
self.advance()
}
Some(value)
}
///|
fn SchemaReader::parse_number(self : SchemaReader) -> String {
let start = self.index
if self.peek() == code('-') {
self.advance()
}
if self.peek() == code('0') {
self.advance()
} else if is_digit(self.peek()) {
while is_digit(self.peek()) {
self.advance()
}
} else {
self.fail(InvalidJsonNumber(self.index))
return "0"
}
if self.peek() == code('.') {
self.advance()
if !is_digit(self.peek()) {
self.fail(InvalidJsonNumber(self.index))
return "0"
}
while is_digit(self.peek()) {
self.advance()
}
}
if self.peek() == code('e') || self.peek() == code('E') {
self.advance()
if self.peek() == code('+') || self.peek() == code('-') {
self.advance()
}
if !is_digit(self.peek()) {
self.fail(InvalidJsonNumber(self.index))
return "0"
}
while is_digit(self.peek()) {
self.advance()
}
}
self.src[start:self.index].to_owned()
}
///|
fn SchemaReader::parse_value(self : SchemaReader) -> J {
if self.failed {
return Null
}
self.skip_ws()
match self.peek().to_char() {
Some('n') =>
if self.consume("null") {
Null
} else {
self.fail(InvalidJson(self.index))
Null
}
Some('t') =>
if self.consume("true") {
Bool(true)
} else {
self.fail(InvalidJson(self.index))
Null
}
Some('f') =>
if self.consume("false") {
Bool(false)
} else {
self.fail(InvalidJson(self.index))
Null
}
Some('"') => Str(self.parse_string())
Some('[') => self.parse_array()
Some('{') => self.parse_object()
Some('-')
| Some('0')
| Some('1')
| Some('2')
| Some('3')
| Some('4')
| Some('5')
| Some('6')
| Some('7')
| Some('8')
| Some('9') => Num(self.parse_number())
_ => {
self.fail(InvalidJson(self.index))
Null
}
}
}
///|
fn SchemaReader::parse_array(self : SchemaReader) -> J {
self.advance()
let items : Array[J] = []
self.skip_ws()
if self.peek() == code(']') {
self.advance()
return Arr(items)
}
while !self.failed {
items.push(self.parse_value())
self.skip_ws()
if self.peek() == code(']') {
self.advance()
return Arr(items)
}
if self.peek() == code(',') {
self.advance()
continue
}
self.fail(InvalidJson(self.index))
return Arr(items)
}
Arr(items)
}
///|
fn SchemaReader::parse_object(self : SchemaReader) -> J {
self.advance()
let fields : Array[(String, J)] = []
self.skip_ws()
if self.peek() == code('}') {
self.advance()
return Obj(fields)
}
while !self.failed {
self.skip_ws()
if self.peek() != code('"') {
self.fail(InvalidJson(self.index))
return Obj(fields)
}
let key = self.parse_string()
self.skip_ws()
if self.peek() != code(':') {
self.fail(InvalidJson(self.index))
return Obj(fields)
}
self.advance()
fields.push((key, self.parse_value()))
self.skip_ws()
if self.peek() == code('}') {
self.advance()
return Obj(fields)
}
if self.peek() == code(',') {
self.advance()
continue
}
self.fail(InvalidJson(self.index))
return Obj(fields)
}
Obj(fields)
}
///|
fn parse_ordered(src : String) -> Result[J, SchemaKind] {
let reader = SchemaReader::{
src,
index: 0,
failed: false,
kind: InvalidJson(0),
}
let value = reader.parse_value()
reader.skip_ws()
if reader.failed {
Err(reader.kind)
} else if !reader.ended() {
Err(InvalidJson(reader.index))
} else {
Ok(value)
}
}
///|
fn object_get(fields : Array[(String, J)], key : String) -> J? {
let mut found : J? = None
for field in fields {
let (name, value) = field
if name == key {
found = Some(value)
}
}
found
}
///|
fn as_fields(value : J) -> Array[(String, J)]? {
match value {
Obj(fields) => Some(fields)
_ => None
}
}
///|
fn json_quote(text : String) -> String {
let buf = StringBuilder()
buf.write_char('"')
let mut index = 0
while index < text.length() {
match text.at(index).to_int().to_char() {
Some('"') => buf.write_string("\\\"")
Some('\\') => buf.write_string("\\\\")
Some('\n') => buf.write_string("\\n")
Some('\r') => buf.write_string("\\r")
Some('\t') => buf.write_string("\\t")
Some(ch) => buf.write_char(ch)
None => ()
}
index += 1
}
buf.write_char('"')
buf.to_string()
}
///|
fn J::display(self : J) -> String {
match self {
Null => "null"
Bool(true) => "true"
Bool(false) => "false"
Num(repr) => repr
Str(text) => json_quote(text)
Arr(items) => {
let buf = StringBuilder()
buf.write_char('[')
let mut index = 0
for item in items {
if index > 0 {
buf.write_char(',')
}
buf.write_string(item.display())
index += 1
}
buf.write_char(']')
buf.to_string()
}
Obj(fields) => {
let buf = StringBuilder()
buf.write_char('{')
let mut index = 0
for field in fields {
if index > 0 {
buf.write_char(',')
}
let (key, value) = field
buf.write_string(json_quote(key))
buf.write_char(':')
buf.write_string(value.display())
index += 1
}
buf.write_char('}')
buf.to_string()
}
}
}
///|
fn join_labels(items : Array[String], separator : String) -> String {
let buf = StringBuilder()
let mut index = 0
for item in items {
if index > 0 {
buf.write_string(separator)
}
buf.write_string(item)
index += 1
}
buf.to_string()
}
///|
fn SchemaShape::label(self : SchemaShape) -> String {
match self {
Any => ""
Str => "string"
Num => "number"
Boolean => "boolean"
Obj => "object"
Arr => "array"
Null => "null"
Ref(name) => name
Enum(items) => join_labels(items, "|")
Union(items) => {
let labels : Array[String] = []
for item in items {
labels.push(item.label())
}
join_labels(labels, " | ")
}
Unknown(name) => name
}
}
///|
fn ParamDef::label(self : ParamDef) -> String {
let marked = if self.required { self.name + "*" } else { self.name }
match self.shape {
Any => marked
_ => marked + ": " + self.shape.label()
}
}
///|
fn push_error(
errors : Array[SchemaError],
path : String,
message : String,
) -> Unit {
errors.push({ path, kind: Expected(message), })
}
///|
fn known_shape(name : String) -> SchemaShape? {
match name {
"string" => Some(Str)
"number" | "integer" => Some(Num)
"boolean" => Some(Boolean)
"object" => Some(Obj)
"array" => Some(Arr)
"null" => Some(Null)
_ => None
}
}
///|
fn ref_name(raw : String) -> String {
let mut start = 0
let mut index = 0
while index < raw.length() {
if raw.at(index).to_int() == code('/') {
start = index + 1
}
index += 1
}
raw[start:].to_owned()
}
///|
fn shape_of(
value : J,
path : String,
errors : Array[SchemaError],
) -> SchemaShape {
match as_fields(value) {
None => {
push_error(errors, path, "expected an object")
Any
}
Some(fields) =>
match object_get(fields, "$ref") {
Some(Str(raw)) => Ref(ref_name(raw))
Some(_) => {
push_error(errors, path + ".$ref", "expected a string")
Any
}
None =>
match object_get(fields, "enum") {
Some(Arr(items)) => {
if items.length() == 0 {
push_error(errors, path + ".enum", "enum must not be empty")
}
let encoded : Array[String] = []
for item in items {
encoded.push(item.display())
}
Enum(encoded)
}
Some(_) => {
push_error(errors, path + ".enum", "expected an array")
Any
}
None =>
match object_get(fields, "const") {
Some(item) => Enum([item.display()])
None => compound_or_type(fields, path, errors)
}
}
}
}
}
///|
fn compound_or_type(
fields : Array[(String, J)],
path : String,
errors : Array[SchemaError],
) -> SchemaShape {
let mut compound : J? = None
let mut compound_key = ""
for key in ["anyOf", "oneOf", "allOf"] {
if compound is None {
match object_get(fields, key) {
Some(value) => {
compound = Some(value)
compound_key = key
}
None => ()
}
}
}
match compound {
Some(Arr(items)) => {
let shapes : Array[SchemaShape] = []
let mut index = 0
for item in items {
shapes.push(
shape_of(
item,
path + "." + compound_key + "." + index.to_string(),
errors,
),
)
index += 1
}
Union(shapes)
}
Some(_) => {
push_error(errors, path + "." + compound_key, "expected an array")
Any
}
None => type_shape(fields, path, errors)
}
}
///|
fn type_shape(
fields : Array[(String, J)],
path : String,
errors : Array[SchemaError],
) -> SchemaShape {
match object_get(fields, "type") {
None => Any
Some(Str(name)) =>
match known_shape(name) {
Some(shape) => shape
None => {
push_error(errors, path + ".type", "unknown type \"" + name + "\"")
Unknown(name)
}
}
Some(Arr(items)) => {
if items.length() == 0 {
push_error(errors, path + ".type", "type must not be empty")
}
let shapes : Array[SchemaShape] = []
let mut index = 0
for item in items {
match item {
Str(name) =>
match known_shape(name) {
Some(shape) => shapes.push(shape)
None => {
push_error(
errors,
path + ".type." + index.to_string(),
"unknown type \"" + name + "\"",
)
shapes.push(Unknown(name))
}
}
_ =>
push_error(
errors,
path + ".type." + index.to_string(),
"expected a string",
)
}
index += 1
}
Union(shapes)
}
Some(_) => {
push_error(
errors,
path + ".type",
"expected a string or an array of strings",
)
Any
}
}
}
///|
fn names_in(fields : Array[(String, J)]) -> Array[String] {
let names : Array[String] = []
for field in fields {
let (name, _) = field
let mut seen = false
for existing in names {
if existing == name {
seen = true
}
}
if !seen {
names.push(name)
}
}
names
}
///|
fn compile_params(
schema : J,
path : String,
errors : Array[SchemaError],
) -> Array[ParamDef] {
match as_fields(schema) {
None => {
push_error(errors, path, "expected an object")
[]
}
Some(fields) => {
let properties = match object_get(fields, "properties") {
None => []
Some(Obj(props)) => props
Some(_) => {
push_error(errors, path + ".properties", "expected an object")
[]
}
}
let required_values = match object_get(fields, "required") {
None => []
Some(Arr(items)) => items
Some(_) => {
push_error(errors, path + ".required", "expected an array")
[]
}
}
let required_names : Array[String] = []
for item in required_values {
match item {
Str(name) => required_names.push(name)
_ => push_error(errors, path + ".required", "expected a string")
}
}
let params : Array[ParamDef] = []
let prop_names = names_in(properties)
for name in prop_names {
match object_get(properties, name) {
Some(value) => {
let mut required = false
for required_name in required_names {
if required_name == name {
required = true
}
}
params.push({
name,
required,
shape: shape_of(value, path + ".properties." + name, errors),
})
}
None => ()
}
}
for required_name in required_names {
let mut found = false
for name in prop_names {
if name == required_name {
found = true
}
}
if !found {
push_error(
errors,
path + ".required",
"\"" + required_name + "\" is not a property",
)
}
}
params
}
}
}
///|
fn is_action_data(name : String, action_names : Array[String]) -> Bool {
if name == "ActionExpression" || name == "OpenUrl" || name == "ToAssistant" {
return true
}
for action_name in action_names {
if action_name == name {
return true
}
}
false
}
///|
fn compile_components(
root : Array[(String, J)],
errors : Array[SchemaError],
) -> Array[ComponentDef] {
let defs = match object_get(root, "$defs") {
None => []
Some(Obj(fields)) => fields
Some(_) => {
push_error(errors, "$.$defs", "expected an object")
[]
}
}
let listed = match object_get(root, "properties") {
None => None
Some(Obj(fields)) => Some(names_in(fields))
Some(_) => {
push_error(errors, "$.properties", "expected an object")
None
}
}
let action_names = match object_get(root, "actions") {
Some(Obj(fields)) => names_in(fields)
_ => []
}
let selected = match listed {
Some(names) => names
None => {
let names : Array[String] = []
for name in names_in(defs) {
if !is_action_data(name, action_names) {
names.push(name)
}
}
names
}
}
let components : Array[ComponentDef] = []
for name in selected {
match object_get(defs, name) {
None => push_error(errors, "$.$defs." + name, "missing component schema")
Some(schema) =>
components.push({
name,
params: compile_params(schema, "$.$defs." + name, errors),
})
}
}
components
}
///|
fn compile_calls(
root : Array[(String, J)],
key : String,
kind : CallKind,
errors : Array[SchemaError],
) -> Array[CallDef] {
let fields = match object_get(root, key) {
None => return []
Some(Obj(fields)) => fields
Some(_) => {
push_error(errors, "$." + key, "expected an object")
return []
}
}
let calls : Array[CallDef] = []
for name in names_in(fields) {
let path = "$." + key + "." + name
match object_get(fields, name) {
Some(Obj(body)) =>
match object_get(body, "params") {
None => push_error(errors, path, "missing params")
Some(params) =>
calls.push({
name,
kind,
params: compile_params(params, path + ".params", errors),
})
}
Some(_) => push_error(errors, path, "expected an object")
None => ()
}
}
calls
}
///|
fn ComponentDef::label(self : ComponentDef) -> String {
let params : Array[String] = []
for param in self.params {
params.push(param.label())
}
self.name + "(" + join_labels(params, ", ") + ")"
}
///|
fn CallDef::label(self : CallDef) -> String {
let kind = match self.kind {
Function => "function"
Action => "action"
}
let params : Array[String] = []
for param in self.params {
params.push(param.label())
}
kind + " " + self.name + "(" + join_labels(params, ", ") + ")"
}
///|
pub fn SchemaCatalog::dump(self : SchemaCatalog) -> String {
let buf = StringBuilder()
buf.write_string("components: " + self.components.length().to_string())
for component in self.components {
buf.write_char('\n')
buf.write_string(component.label())
}
buf.write_char('\n')
buf.write_string("calls: " + self.calls.length().to_string())
for call in self.calls {
buf.write_char('\n')
buf.write_string(call.label())
}
buf.write_char('\n')
buf.write_string("errors: " + self.errors.length().to_string())
for err in self.errors {
buf.write_char('\n')
buf.write_string(err.path + ": " + err.message())
}
buf.to_string()
}
///|
/// Compile the component definition table from a library JSON Schema document.
/// Property order is the positional order of OpenUI Lang arguments.
pub fn compile_schema(src : String) -> SchemaCatalog {
match parse_ordered(src) {
Err(kind) => { components: [], calls: [], errors: [{ path: "$", kind, }], }
Ok(Obj(root)) => {
let errors : Array[SchemaError] = []
let components = compile_components(root, errors)
let functions = compile_calls(root, "functions", Function, errors)
let actions = compile_calls(root, "actions", Action, errors)
let calls = functions
for action in actions {
calls.push(action)
}
{ components, calls, errors, }
}
Ok(_) =>
{
components: [],
calls: [],
errors: [{ path: "$", kind: Expected("expected an object"), }],
}
}
}