use std::collections::{HashMap, HashSet};
use std::iter;
use std::sync::LazyLock;
use crate::assemble::{ASM, ASMSchema, ASMState, build_asm_graph};
use crate::build::{SymbolGraph, SymbolVar, build_symbol_graph};
use crate::guide::{Trail, TrailState};
use crate::helpers::{TrailError, is_escaped};
const KEYWORDS: LazyLock<HashSet<&str>> = LazyLock::new(|| {
HashSet::from([
"type",
"enum",
"const",
"properties",
"required",
"items",
"prefixItems",
"oneOf",
])
});
#[derive(Clone)]
pub struct InputContext {
content: String,
line: usize,
path: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputKind {
Block,
Entry,
Array,
String,
Number,
Integer,
}
#[derive(Clone)]
pub struct JSONInput {
kind: InputKind,
context: InputContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LabelKind {
Property,
Keyword,
}
#[derive(Clone)]
pub struct JSONLabel {
kind: LabelKind,
context: InputContext,
}
type JSONSpecs = HashMap<String, JSONInput>;
fn split_json_input(input: JSONInput) -> Result<Vec<JSONInput>, TrailError> {
let (context, kind) = (&input.context, &input.kind);
let (content, path) = (&context.content, &context.path);
assert!(
matches!(kind, InputKind::Block | InputKind::Array),
"Expected `Block` or `Array`, got `{:#?}` instead.",
{ kind }
);
let mut brackets = 0;
let mut braces = 0;
let mut inputs: Vec<JSONInput> = Vec::new();
let mut i = 0;
let mut accumulate: Vec<char> = Vec::new();
let mut line = context.line;
let chars: Vec<char> = content[1..content.len() - 1].chars().collect();
while i < chars.len() {
let curr = chars[i];
if curr == '{' {
braces += 1;
} else if curr == '}' {
braces -= 1;
} else if curr == '[' {
brackets += 1;
} else if curr == ']' {
brackets -= 1;
} else if curr == '\n' {
line += 1;
} else if curr == ',' && braces == 0 && brackets == 0 {
consume_input(&mut inputs, &mut accumulate, line, path.clone())?;
i += 1;
continue;
}
accumulate.push(curr);
i += 1;
}
if !is_valid_whitespace(&accumulate) {
consume_input(&mut inputs, &mut accumulate, line, path.clone())?;
}
return Ok(inputs);
}
pub fn build_entry_from_input(input: JSONInput) -> Result<(JSONLabel, JSONInput), TrailError> {
let (context, kind) = (&input.context, &input.kind);
assert!(
matches!(kind, InputKind::Entry),
"Expected `Entry`, got `{:#?}` instead.",
{ kind }
);
let (line, path) = (context.line, &context.path);
let (key, value) = context
.content
.split_once(':')
.map(|(l, r)| (l.trim(), r.trim()))
.expect("Failed to split input of kind `Entry`.");
if is_valid_string(key) {
let label = build_json_label(&key[1..key.len() - 1], line, path.clone());
let value = build_json_input(value, line, format!("{}/{}", &path, &key[1..key.len() - 1]))?;
return Ok((label, value));
} else {
return Err(TrailError(format_json_error(
JSONError::InvalidLabel,
input.context,
)));
}
}
pub fn build_specs_from_input(input: JSONInput) -> Result<JSONSpecs, TrailError> {
let mut specs: JSONSpecs = HashMap::new();
if input.kind != InputKind::Block {
return Err(TrailError(format_json_error(
JSONError::InvalidBlock,
input.context,
)));
}
let entries = split_json_input(input)?;
for entry in entries {
if entry.kind != InputKind::Entry {
return Err(TrailError(format_json_error(
JSONError::InvalidEntry,
entry.context,
)));
}
let (label, input) = build_entry_from_input(entry)?;
let context = label.context;
if label.kind != LabelKind::Keyword {
return Err(TrailError(format_json_error(
JSONError::InvalidKeyword,
context,
)));
}
specs.insert(context.content, input);
}
return Ok(specs);
}
#[derive(Clone)]
pub struct JSONBlock {
id: SymbolVar,
entity: Box<JSONEntity>,
}
impl Default for JSONBlock {
fn default() -> Self {
Self {
id: SymbolVar::rand(),
entity: Box::new(JSONEntity::default()),
}
}
}
#[derive(Clone)]
pub struct JSONProperty {
label: String,
block: JSONBlock,
required: bool,
}
#[derive(Clone)]
pub enum JSONEntity {
Object { properties: Vec<JSONProperty> },
Union { blocks: Vec<JSONBlock> },
Array { blocks: Vec<JSONBlock> },
Tuple { blocks: Vec<JSONBlock> },
Literal { value: String },
}
impl Default for JSONEntity {
fn default() -> Self {
Self::Literal {
value: String::from("\"null\""),
}
}
}
pub fn build_entity_from_input(input: JSONInput) -> Result<JSONEntity, TrailError> {
let specs = build_specs_from_input(input)?;
if let Some(input) = specs.get("type") {
let label = input.clone();
let (context, kind) = (label.context, label.kind);
if kind != InputKind::String {
return Err(TrailError(format_json_error(
JSONError::InvalidLabel,
context,
)));
}
let content = &context.content[1..context.content.len() - 1];
let entity = if content == "object" {
build_object_from_specs(&specs)?
} else if content == "array" {
build_array_from_specs(&specs)?
} else if content == "string" {
build_string_from_specs(&specs)?
} else if content == "number" {
build_number_from_specs(&specs)?
} else if content == "integer" {
build_integer_from_specs(&specs)?
} else if content == "boolean" {
JSONEntity::Literal {
value: String::from("\"true\" | \"false\""),
}
} else if content == "null" {
JSONEntity::Literal {
value: String::from("\"null\""),
}
} else {
return Err(TrailError(format_json_error(
JSONError::InvalidType,
context,
)));
};
return Ok(entity);
}
if let Some(input) = specs.get("oneOf") {
let array = input.clone();
if input.kind != InputKind::Array {
return Err(TrailError(format_json_error(
JSONError::InvalidArray,
array.context,
)));
}
let inputs: Vec<JSONInput> = split_json_input(array)?
.into_iter()
.map(|block| {
if block.kind != InputKind::Block {
Err(TrailError(format_json_error(
JSONError::InvalidBlock,
block.context,
)))
} else {
Ok(block)
}
})
.collect::<Result<Vec<JSONInput>, TrailError>>()?;
let mut blocks: Vec<JSONBlock> = Vec::new();
for input in inputs {
let entity = build_entity_from_input(input)?;
let block = JSONBlock {
id: SymbolVar::rand(),
entity: Box::new(entity),
};
blocks.push(block)
}
return Ok(JSONEntity::Union { blocks: blocks });
}
return Ok(JSONEntity::default());
}
pub fn build_object_from_specs(specs: &JSONSpecs) -> Result<JSONEntity, TrailError> {
let mut properties: Vec<JSONProperty> = Vec::new();
if let Some(input) = specs.get("properties") {
let block = input.clone();
if block.kind != InputKind::Block {
return Err(TrailError(format_json_error(
JSONError::InvalidBlock,
block.context,
)));
}
let entries = split_json_input(block)?;
for entry in entries {
if entry.kind != InputKind::Entry {
return Err(TrailError(format_json_error(
JSONError::InvalidEntry,
entry.context,
)));
}
let (label, input) = build_entry_from_input(entry)?;
let context = label.context;
let entity = build_entity_from_input(input)?;
let block = JSONBlock {
id: SymbolVar::rand(),
entity: Box::new(entity),
};
properties.push(JSONProperty {
label: context.content,
block: block,
required: false,
})
}
}
if let Some(input) = specs.get("required") {
let array = input.clone();
if array.kind != InputKind::Array {
return Err(TrailError(format_json_error(
JSONError::InvalidInput,
array.context,
)));
}
let labels: Vec<String> = split_json_input(array)?
.into_iter()
.map(|label| {
let context = label.context;
if label.kind != InputKind::String {
Err(TrailError(format_json_error(
JSONError::InvalidString,
context,
)))
} else {
Ok(context.content[1..context.content.len() - 1].to_string())
}
})
.collect::<Result<Vec<String>, TrailError>>()?;
for property in &mut properties {
if labels.contains(&property.label) {
property.required = true;
}
}
}
return Ok(JSONEntity::Object {
properties: properties,
});
}
pub fn build_array_from_specs(specs: &JSONSpecs) -> Result<JSONEntity, TrailError> {
if let Some(input) = specs.get("items") {
let (block, kind) = (input.clone(), input.kind);
if kind != InputKind::Block {
return Err(TrailError(format_json_error(
JSONError::InvalidBlock,
block.context,
)));
}
let entity = build_entity_from_input(block)?;
let block = JSONBlock {
id: SymbolVar::rand(),
entity: Box::new(entity),
};
return Ok(JSONEntity::Array {
blocks: vec![block],
});
}
if let Some(input) = specs.get("prefixItems") {
let (array, kind) = (input.clone(), input.kind);
if kind != InputKind::Array {
return Err(TrailError(format_json_error(
JSONError::InvalidArray,
array.context,
)));
}
let inputs: Vec<JSONInput> = split_json_input(array)?
.into_iter()
.map(|block| {
if block.kind != InputKind::Block {
Err(TrailError(format_json_error(
JSONError::InvalidBlock,
block.context,
)))
} else {
Ok(block)
}
})
.collect::<Result<Vec<JSONInput>, TrailError>>()?;
if inputs.is_empty() {
return Ok(JSONEntity::Tuple {
blocks: vec![JSONBlock::default()],
});
}
let mut blocks: Vec<JSONBlock> = Vec::new();
for input in inputs {
let entity = build_entity_from_input(input)?;
let block = JSONBlock {
id: SymbolVar::rand(),
entity: Box::new(entity),
};
blocks.push(block)
}
return Ok(JSONEntity::Tuple { blocks: blocks });
}
return Ok(JSONEntity::Array {
blocks: vec![JSONBlock::default()],
});
}
pub fn build_number_from_specs(specs: &JSONSpecs) -> Result<JSONEntity, TrailError> {
if let Some(input) = specs.get("const") {
let number = input.clone();
let (context, kind) = (number.context, number.kind);
if !matches!(kind, InputKind::Integer | InputKind::Number) {
return Err(TrailError(format_json_error(
JSONError::InvalidNumber,
context,
)));
}
return Ok(JSONEntity::Literal {
value: format!("\"{}\"", context.content),
});
}
if let Some(input) = specs.get("enum") {
let array = input.clone();
if array.kind != InputKind::Array {
return Err(TrailError(format_json_error(
JSONError::InvalidArray,
array.context,
)));
}
let value: String = split_json_input(array)?
.into_iter()
.map(|label| {
let context = label.context;
if !matches!(label.kind, InputKind::Integer | InputKind::Number) {
Err(TrailError(format_json_error(
JSONError::InvalidNumber,
context,
)))
} else {
Ok(format!("\"{}\"", context.content))
}
})
.collect::<Result<Vec<String>, TrailError>>()?
.join(" | ");
return Ok(JSONEntity::Literal { value: value });
}
return Ok(JSONEntity::Literal {
value: String::from("/^[0-9]\\.[0-9]$/"),
});
}
pub fn build_integer_from_specs(specs: &JSONSpecs) -> Result<JSONEntity, TrailError> {
if let Some(input) = specs.get("const") {
let number = input.clone();
let (context, kind) = (number.context, number.kind);
if kind != InputKind::Integer {
return Err(TrailError(format_json_error(
JSONError::InvalidInteger,
context,
)));
}
return Ok(JSONEntity::Literal {
value: format!("\"{}\"", context.content),
});
}
if let Some(input) = specs.get("enum") {
let array = input.clone();
if array.kind != InputKind::Array {
return Err(TrailError(format_json_error(
JSONError::InvalidArray,
array.context,
)));
}
let value: String = split_json_input(array)?
.into_iter()
.map(|label| {
let context = label.context;
if label.kind != InputKind::Integer {
Err(TrailError(format_json_error(
JSONError::InvalidInteger,
context,
)))
} else {
Ok(format!("\"{}\"", context.content))
}
})
.collect::<Result<Vec<String>, TrailError>>()?
.join(" | ");
return Ok(JSONEntity::Literal { value: value });
}
return Ok(JSONEntity::Literal {
value: String::from("/^[0-9]$/"),
});
}
pub fn build_string_from_specs(specs: &JSONSpecs) -> Result<JSONEntity, TrailError> {
if let Some(input) = specs.get("const") {
let string = input.clone();
let (context, kind) = (string.context, string.kind);
if kind != InputKind::String {
return Err(TrailError(format_json_error(
JSONError::InvalidString,
context,
)));
}
return Ok(JSONEntity::Literal {
value: format!(
"\"\\\"{}\\\"\"",
&context.content[1..context.content.len() - 1]
),
});
}
if let Some(input) = specs.get("enum") {
let array = input.clone();
if array.kind != InputKind::Array {
return Err(TrailError(format_json_error(
JSONError::InvalidArray,
array.context,
)));
}
let value: String = split_json_input(array)?
.into_iter()
.map(|label| {
let context = label.context;
if label.kind != InputKind::String {
Err(TrailError(format_json_error(
JSONError::InvalidString,
context,
)))
} else {
let content = &context.content[1..context.content.len() - 1];
Ok(format!("\"\\\"{}\\\"\"", content))
}
})
.collect::<Result<Vec<String>, TrailError>>()?
.join(" | ");
return Ok(JSONEntity::Literal { value: value });
}
return Ok(JSONEntity::Literal {
value: String::from("/\"PLACEHOLDER\"/"),
});
}
type CFGGraph = HashMap<SymbolVar, SymbolGraph>;
pub fn build_cfg_from_entity(entity: JSONEntity) -> CFGGraph {
let mut graphs: CFGGraph = HashMap::new();
let mut items: Vec<JSONBlock> = Vec::new();
items.push(JSONBlock {
id: SymbolVar::new("start"),
entity: Box::new(entity),
});
while !items.is_empty() {
let item = items.pop().unwrap();
match *item.entity {
JSONEntity::Object { properties } => {
let mut production = format!("\"{{\"");
let mut unions: Vec<String> = Vec::new();
let mut anchor = false;
for property in &properties {
let block = &property.block;
for union in &mut unions {
union.push_str(&format!(
"( \",\" \"\\\"{}\\\":\" {} )",
property.label, block.id.0,
));
}
if property.required {
if unions.is_empty() {
unions.push(format!(
"( \"\\\"{}\\\":\" {} )",
property.label, block.id.0
));
}
anchor = true;
} else {
for union in &mut unions {
union.push_str("?");
}
if !anchor {
unions.push(format!(
"( \"\\\"{}\\\":\" {} )",
property.label, block.id.0
));
}
}
items.push(block.clone());
}
if !anchor {
unions.push(String::from("\"\""));
}
production.push_str(&format!("({})", unions.join("|")));
production.push_str(&format!("\"}}\""));
let graph = build_symbol_graph(&production)
.expect("Expected `SymbolGraph`, but got a `TrailError`.");
graphs.insert(item.id, graph);
}
JSONEntity::Array { blocks } => {
let mut production = String::from(" \"[\" ( ");
for block in &blocks[..blocks.len() - 1] {
production.push_str(&format!(" {} \",\" | ", block.id.0));
items.push(block.clone());
}
if let Some(last_block) = blocks.last() {
production.push_str(&format!(
" {} \",\" )* {} \"]\" ",
last_block.id.0, last_block.id.0
));
items.push(last_block.clone());
}
let graph = build_symbol_graph(&production)
.expect("Expected `SymbolGraph`, but got a `TrailError`.");
graphs.insert(item.id, graph);
}
JSONEntity::Tuple { blocks } => {
let mut production = String::from(" \"[\" ");
for block in &blocks[0..blocks.len() - 1] {
production.push_str(&format!(" {} \",\" ", block.id.0));
items.push(block.clone());
}
if let Some(last_block) = blocks.last() {
production.push_str(&format!(" {} \"]\" ", last_block.id.0));
items.push(last_block.clone());
}
let graph = build_symbol_graph(&production)
.expect("Expected `SymbolGraph`, but got a `TrailError`.");
graphs.insert(item.id, graph);
}
JSONEntity::Union { blocks } => {
let mut production = String::from("(");
for block in &blocks[0..blocks.len() - 1] {
production.push_str(&format!(" {} | ", block.id.0));
items.push(block.clone());
}
if let Some(last_block) = blocks.last() {
production.push_str(&format!(" {} ) ", last_block.id.0));
items.push(last_block.clone());
}
let graph = build_symbol_graph(&production)
.expect("Expected `SymbolGraph`, but got a `TrailError`.");
graphs.insert(item.id, graph);
}
JSONEntity::Literal { value } => {
let graph = build_symbol_graph(&format!("{}", value))
.expect("Expected `SymbolGraph`, but got a `TrailError`.");
graphs.insert(item.id.clone(), graph);
}
}
}
return graphs;
}
pub fn trail_json<'a>(schema: &str) -> Result<Trail<'a>, TrailError> {
let input = build_json_input(schema.trim(), 0, String::from("~"))?;
let entity = build_entity_from_input(input)?;
return Ok((build_cfg_from_entity(entity), TrailState::default()));
}
pub fn asm_json<'a>(schema: &str, alphabet: Vec<String>) -> Result<ASM<'a>, TrailError> {
let input = build_json_input(schema.trim(), 0, String::new())?;
let entity = build_entity_from_input(input)?;
return Ok((
ASMSchema {
cfg: build_cfg_from_entity(entity),
asm: build_asm_graph(alphabet),
},
ASMState::default(),
));
}
pub enum JSONError {
InvalidInput,
InvalidLabel,
InvalidEntry,
InvalidBlock,
InvalidKeyword,
InvalidNumber,
InvalidArray,
InvalidString,
InvalidInteger,
InvalidType,
}
impl JSONError {
pub const fn message(&self) -> &'static str {
match self {
Self::InvalidInput => "Invalid JSON input.",
Self::InvalidLabel => "Invalid JSON label.",
Self::InvalidKeyword => "Invalid JSON keyword.",
Self::InvalidBlock => "Invalid JSON block.",
Self::InvalidArray => "Invalid JSON array.",
Self::InvalidEntry => "Invalid JSON entry.",
Self::InvalidString => "Invalid JSON string.",
Self::InvalidNumber => "Invalid JSON number.",
Self::InvalidInteger => "Invalid JSON integer.",
Self::InvalidType => "Invalid JSON type.",
}
}
pub const fn help(&self) -> &'static str {
match self {
Self::InvalidInput => {
"Expected format: value ({}, [], \"string\", number) or \"key\" : value pair."
}
Self::InvalidLabel => "Expected format: \"name\" (alphanumeric + underscore only).",
Self::InvalidKeyword => {
"Valid keywords include: type, const, enum, properties, items, required, etc."
}
Self::InvalidBlock => {
"Expected format: {value}, where value is {object}, [array], \"string\", or number."
}
Self::InvalidArray => {
"Expected format: [value], where value is {object}, [array], \"string\", or number."
}
Self::InvalidEntry => {
"Expected format: \"key\" : value, where value is {object}, [array], \"string\", or number."
}
Self::InvalidString => {
"Expected format: \"name\". Inner quotes must be escaped - use \\\" instead of \"."
}
Self::InvalidNumber => "Expected a float number.",
Self::InvalidInteger => "Expected an integer number.",
Self::InvalidType => "Valid types include: object, array, number, string, ect.",
}
}
}
pub fn consume_input(
inputs: &mut Vec<JSONInput>,
accumulate: &mut Vec<char>,
line: usize,
path: String,
) -> Result<(), TrailError> {
let content: String = accumulate.iter().collect();
let input = build_json_input(content.trim(), line.clone(), path.clone())?;
inputs.push(input);
accumulate.clear();
return Ok(());
}
pub fn build_json_input(content: &str, line: usize, path: String) -> Result<JSONInput, TrailError> {
let context = InputContext {
content: content.to_string(),
line: line,
path: path,
};
let kind = if is_valid_block(&content) {
InputKind::Block
} else if is_valid_array(&content) {
InputKind::Array
} else if is_valid_entry(&content) {
InputKind::Entry
} else if is_valid_string(&content) {
InputKind::String
} else if is_valid_integer(&content) {
InputKind::Integer
} else if is_valid_number(&content) {
InputKind::Number
} else {
return Err(TrailError(format_json_error(
JSONError::InvalidInput,
context,
)));
};
return Ok(JSONInput {
kind: kind,
context: context,
});
}
pub fn build_json_label(content: &str, line: usize, path: String) -> JSONLabel {
let context = InputContext {
content: content.to_string(),
line: line,
path: path,
};
let kind = if KEYWORDS.contains(content) {
LabelKind::Keyword
} else {
LabelKind::Property
};
return JSONLabel {
kind: kind,
context: context,
};
}
pub fn is_valid_block(input: &str) -> bool {
return input.starts_with('{') && input.ends_with('}');
}
pub fn is_valid_entry(input: &str) -> bool {
return input.contains(':');
}
pub fn is_valid_array(input: &str) -> bool {
return input.starts_with('[') && input.ends_with(']');
}
pub fn is_valid_string(input: &str) -> bool {
return input.starts_with('"')
&& input.ends_with('"')
&& input[1..input.len() - 1]
.split("\\\"")
.all(|part| !part.contains('"'));
}
pub fn is_valid_number(input: &str) -> bool {
return input.parse::<f64>().is_ok();
}
pub fn is_valid_integer(input: &str) -> bool {
return input.parse::<u64>().is_ok();
}
pub fn is_valid_whitespace(chars: &[char]) -> bool {
chars.iter().all(|c| c.is_whitespace())
}
pub fn format_json_instance(instance: &str) -> String {
let mut chunks: Vec<char> = Vec::new();
let mut depth = 0;
let mut in_quote = false;
let mut i = 0;
let chars: Vec<char> = instance.chars().collect();
while i < chars.len() {
let curr = chars[i];
assert!(
![' ', '\n', '\t', '\r'].contains(&curr),
"There should be no whitespace characters."
);
if curr == '"' {
if !is_escaped(&chars, i) {
in_quote = !in_quote;
}
chunks.push(curr)
} else if matches!(curr, '{' | '[') {
chunks.extend([curr, '\n']);
depth += 1;
chunks.extend(iter::repeat('\t').take(depth))
} else if matches!(curr, '}' | ']') {
depth -= 1;
chunks.extend(
iter::once('\n')
.chain(iter::repeat('\t').take(depth))
.chain(iter::once(curr)),
);
} else if curr == ',' {
chunks.extend(
iter::once(curr)
.chain(iter::once('\n'))
.chain(iter::repeat('\t').take(depth)),
);
} else if curr == ':' {
chunks.extend([curr, ' '])
} else {
chunks.push(curr);
}
i += 1
}
return chunks.into_iter().collect();
}
pub fn format_json_error(error: JSONError, context: InputContext) -> String {
const RED: &str = "\x1b[31m";
const BLUE: &str = "\x1b[34m";
const CYAN: &str = "\x1b[36m";
const BOLD: &str = "\x1b[1m";
const RESET: &str = "\x1b[0m";
let (path, content, line) = (&context.path, &context.content, &context.line);
let (help, message) = (error.help(), error.message());
let context = content
.lines()
.enumerate()
.map(|(i, content)| format!("{BOLD}{BLUE}{:5} |{RESET} {}", line + i, content))
.collect::<Vec<_>>()
.join("\n");
format!(
"{BOLD}{RED}error{RESET}: {message}\n\
{BOLD}{BLUE} --> {RESET}{path}\n\
{BOLD}{BLUE} |{RESET}\n\
{context}\n\
{BOLD}{BLUE} |{RESET}\n\
{BOLD}{BLUE} = {BOLD}{CYAN}help{RESET}: {help}"
)
}