///|
/// Error returned when parsing a compact schema specification.
pub(all) struct SchemaSpecError {
code : String
message : String
column : Int
} derive(Eq)
///|
fn schema_spec_error(
code : String,
message : String,
column : Int,
) -> SchemaSpecError {
{ code, message, column }
}
///|
fn split_on_char(text : String, delimiter : Char) -> Array[String] {
let parts : Array[String] = []
let mut start = 0
let mut i = 0
while i < text.length() {
match text.get_char(i) {
Some(c) =>
if c == delimiter {
parts.push(text[start:i].to_owned())
start = i + 1
}
None => ()
}
i = i + 1
}
parts.push(text[start:text.length()].to_owned())
parts
}
///|
fn find_char(text : String, target : Char) -> Int? {
let mut i = 0
while i < text.length() {
match text.get_char(i) {
Some(c) => if c == target { return Some(i) }
None => ()
}
i = i + 1
}
None
}
///|
fn parse_kind_label(label : String) -> FieldKind? {
match label.trim().to_owned().to_lower() {
"text" => Some(Text)
"string" => Some(Text)
"non_empty" => Some(NonEmpty)
"nonempty" => Some(NonEmpty)
"required" => Some(NonEmpty)
"int" => Some(Integer)
"integer" => Some(Integer)
"decimal" => Some(Decimal)
"number" => Some(Decimal)
"bool" => Some(Boolean)
"boolean" => Some(Boolean)
_ => None
}
}
///|
fn strip_required_suffix(kind_text : String) -> (String, Bool) {
let trimmed = kind_text.trim().to_owned()
if trimmed.is_empty() {
("", true)
} else {
let last_index = trimmed.length() - 1
match trimmed.get_char(last_index) {
Some('!') => (trimmed[0:last_index].to_owned().trim().to_owned(), true)
Some('?') => (trimmed[0:last_index].to_owned().trim().to_owned(), false)
_ => (trimmed, true)
}
}
}
///|
fn parse_column_rule(
token : String,
index : Int,
) -> Result[ColumnRule, SchemaSpecError] {
let trimmed = token.trim().to_owned()
if trimmed.is_empty() {
return Err(schema_spec_error("empty_rule", "schema rule is empty", index))
}
match find_char(trimmed, ':') {
Some(colon) => {
let name = trimmed[0:colon].to_owned().trim().to_owned()
let raw_kind = trimmed[colon + 1:trimmed.length()].to_owned()
let (kind_text, required) = strip_required_suffix(raw_kind)
if name.is_empty() {
return Err(
schema_spec_error("empty_name", "schema column name is empty", index),
)
}
match parse_kind_label(kind_text) {
Some(kind) => Ok({ name, kind, required })
None =>
Err(
schema_spec_error(
"unknown_kind", "unknown schema field kind", index,
),
)
}
}
None =>
Err(
schema_spec_error(
"missing_kind", "schema rule must use name:kind", index,
),
)
}
}
///|
fn has_schema_rule_name(columns : Array[ColumnRule], name : String) -> Bool {
for rule in columns {
if rule.name == name {
return true
}
}
false
}
///|
/// Parse a compact schema specification.
///
/// Example: `name:non_empty!,age:int?,active:bool`.
pub fn parse_schema_spec(
spec : String,
allow_extra_columns : Bool,
) -> Result[Schema, SchemaSpecError] {
let columns : Array[ColumnRule] = []
let parts = split_on_char(spec, ',')
let mut i = 0
while i < parts.length() {
match parse_column_rule(parts[i], i) {
Ok(rule) => {
if has_schema_rule_name(columns, rule.name) {
return Err(
schema_spec_error(
"duplicate_name", "schema column name is duplicated", i,
),
)
}
columns.push(rule)
}
Err(err) => return Err(err)
}
i = i + 1
}
Ok({ columns, allow_extra_columns })
}
///|
/// Serialize a schema back to the compact `name:kind!` form.
pub fn schema_to_spec(schema : Schema) -> String {
let out = StringBuilder::StringBuilder()
let mut i = 0
while i < schema.columns.length() {
if i > 0 {
out.write_char(',')
}
let rule = schema.columns[i]
out.write_string(rule.name)
out.write_char(':')
out.write_string(kind_name(rule.kind))
if rule.required {
out.write_char('!')
} else {
out.write_char('?')
}
i = i + 1
}
out.to_string()
}