use std::collections::{BTreeMap, HashMap};
use std::fmt::{self, Display, Formatter};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::compile::{
AliasCategory, CwtAlias, CwtComplexEnum, CwtFieldAttributes, CwtLink, CwtRuleCondition,
CwtRuleField, CwtRuleOrigin, CwtRuleValue, CwtSchemaGraph, CwtScope, CwtSeverity, CwtSubtype,
SchemaRootDefinition, SchemaRootId, SchemaRootKeyFilter,
};
use super::error::CwtLoadError;
use super::rule_path::SchemaDirectory;
use super::source::SchemaPack;
use crate::DATA_VERSION;
use crate::model::{GamePath, GamePathBuf};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct CwtNodeId(pub String);
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SchemaBinding {
Bound {
type_id: SchemaRootId,
node_id: CwtNodeId,
},
Dynamic {
reason: &'static str,
},
Unbound {
reason: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledRulePack {
pub format_version: String,
pub source_id: Option<String>,
pub roots: Vec<CompiledRoot>,
pub aliases: Vec<CompiledAlias>,
pub declared_modifiers: Vec<CompiledAlias>,
pub enums: Vec<CompiledStringSet>,
pub value_sets: Vec<CompiledStringSet>,
pub complex_enums: Vec<CompiledComplexEnum>,
pub links: Vec<CompiledLink>,
pub scopes: Vec<String>,
pub scope_definitions: Vec<CompiledScope>,
}
impl CompiledRulePack {
pub fn from_graph(graph: &CwtSchemaGraph) -> Self {
let mut roots = graph
.types
.values()
.map(CompiledRoot::from_type_def)
.collect::<Vec<_>>();
roots.sort_by(|left, right| left.name.cmp(&right.name));
let mut aliases = graph
.aliases
.values()
.flatten()
.map(CompiledAlias::from_alias)
.collect::<Vec<_>>();
aliases.sort_by(|left, right| {
left.category
.as_str()
.cmp(right.category.as_str())
.then_with(|| left.name.cmp(&right.name))
});
let declared_modifiers = graph
.modifiers
.iter()
.filter(|(name, _)| {
!graph
.aliases
.contains_key(&(AliasCategory::Modifier, (*name).clone()))
})
.map(|(name, scope)| CompiledAlias::declared_modifier(name, scope))
.collect();
Self {
format_version: DATA_VERSION.to_string(),
source_id: None,
roots,
aliases,
declared_modifiers,
enums: sorted_string_sets(&graph.enums),
value_sets: sorted_string_sets(&graph.value_sets),
complex_enums: sorted_complex_enums(&graph.complex_enums),
links: sorted_links(&graph.links),
scopes: graph.scopes.clone(),
scope_definitions: sorted_scope_definitions(graph.scope_definitions()),
}
}
pub fn from_schema_pack(pack: &SchemaPack) -> Self {
let mut compiled = Self::from_graph(pack.graph.as_ref());
compiled.source_id = Some(pack.id.to_hex());
compiled
}
pub(crate) fn to_bytes(&self) -> Result<Vec<u8>, CwtLoadError> {
bincode::serialize(self).map_err(|error| CwtLoadError::Codec {
message: error.to_string(),
})
}
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, CwtLoadError> {
let pack: Self = bincode::deserialize(bytes).map_err(|error| CwtLoadError::Codec {
message: error.to_string(),
})?;
if pack.format_version != DATA_VERSION {
return Err(CwtLoadError::Codec {
message: format!(
"compiled pack format `{}` is not supported by `{DATA_VERSION}`",
pack.format_version
),
});
}
Ok(pack)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledRoot {
pub name: String,
pub path: Option<SchemaDirectory>,
pub path_file: Option<GamePathBuf>,
pub name_field: Option<String>,
pub localisation: Vec<CompiledRuleField>,
pub type_key_filter: Option<CompiledTypeKeyFilter>,
pub push_scope: Option<String>,
pub type_per_file: bool,
pub name_from_file: bool,
pub skip_root_keys: Vec<String>,
pub subtypes: Vec<CompiledSubtype>,
pub rules: Vec<CompiledRuleField>,
pub origin: CompiledRuleOrigin,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledComplexEnum {
pub name: String,
pub path: Option<SchemaDirectory>,
pub path_file: Option<GamePathBuf>,
pub start_from_root: bool,
pub name_rules: Vec<CompiledRuleField>,
}
impl CompiledComplexEnum {
pub fn matches(&self, file: &GamePath) -> bool {
self.path.as_ref().is_some_and(|directory| {
directory
.match_depth(self.path_file.as_deref(), file)
.is_some()
})
}
fn from_complex_enum(complex_enum: &CwtComplexEnum) -> Self {
Self {
name: complex_enum.name.clone(),
path: complex_enum.path.clone(),
path_file: complex_enum.path_file.clone(),
start_from_root: complex_enum.start_from_root,
name_rules: complex_enum
.name_rules
.iter()
.map(CompiledRuleField::from_rule_field)
.collect(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledLink {
pub name: String,
pub input_scopes: Vec<String>,
pub output_scope: Option<String>,
}
impl CompiledLink {
fn from_link(link: &CwtLink) -> Self {
let mut input_scopes = link.input_scopes.clone();
input_scopes.sort();
input_scopes.dedup();
Self {
name: link.name.clone(),
input_scopes,
output_scope: link.output_scope.clone(),
}
}
}
impl CompiledRoot {
fn match_depth(&self, file: &GamePath) -> Option<usize> {
self.path
.as_ref()?
.match_depth(self.path_file.as_deref(), file)
}
fn from_type_def(definition: &SchemaRootDefinition) -> Self {
Self {
name: definition.name.as_str().to_string(),
path: definition.path.clone(),
path_file: definition.path_file.clone(),
name_field: definition.name_field.clone(),
localisation: definition
.localisation
.iter()
.map(CompiledRuleField::from_rule_field)
.collect(),
type_key_filter: definition
.type_key_filter
.as_ref()
.map(CompiledTypeKeyFilter::from_schema),
push_scope: definition.push_scope.clone(),
type_per_file: definition.type_per_file,
name_from_file: definition.name_from_file,
skip_root_keys: definition.skip_root_keys.clone(),
subtypes: definition
.subtypes
.iter()
.map(CompiledSubtype::from_subtype)
.collect(),
rules: definition
.rules
.iter()
.map(CompiledRuleField::from_rule_field)
.collect(),
origin: CompiledRuleOrigin::from_schema(definition.origin),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledSubtype {
pub name: String,
pub attributes: CompiledFieldAttributes,
pub type_key_filter: Option<CompiledTypeKeyFilter>,
pub rules: Vec<CompiledRuleField>,
}
impl CompiledSubtype {
fn from_subtype(subtype: &CwtSubtype) -> Self {
Self {
name: subtype.name.clone(),
attributes: CompiledFieldAttributes::from_attributes(&subtype.attributes),
type_key_filter: subtype
.type_key_filter
.as_ref()
.map(CompiledTypeKeyFilter::from_schema),
rules: subtype
.rules
.iter()
.map(CompiledRuleField::from_rule_field)
.collect(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CompiledTypeKeyFilter {
Exact(Vec<String>),
Exclude(Vec<String>),
Prefix(String),
}
impl CompiledTypeKeyFilter {
fn from_schema(filter: &SchemaRootKeyFilter) -> Self {
match filter {
SchemaRootKeyFilter::Exact(values) => Self::Exact(values.clone()),
SchemaRootKeyFilter::Exclude(values) => Self::Exclude(values.clone()),
SchemaRootKeyFilter::Prefix(prefix) => Self::Prefix(prefix.clone()),
}
}
pub fn matches(&self, key: &str) -> bool {
match self {
Self::Exact(values) => values.iter().any(|value| value == key),
Self::Exclude(values) => values.iter().all(|value| value != key),
Self::Prefix(prefix) => key.starts_with(prefix.as_str()),
}
}
fn primary_label(&self) -> Option<&str> {
match self {
Self::Exact(values) => values.first().map(String::as_str),
Self::Exclude(_) | Self::Prefix(_) => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledAlias {
pub category: CompiledAliasCategory,
pub name: String,
pub attributes: CompiledFieldAttributes,
pub value: CompiledRuleValue,
pub rules: Vec<CompiledRuleField>,
pub origin: CompiledRuleOrigin,
}
impl CompiledAlias {
fn declared_modifier(name: &str, scope: &str) -> Self {
Self {
category: CompiledAliasCategory::Modifier,
name: name.to_string(),
attributes: CompiledFieldAttributes::from_attributes(&CwtFieldAttributes {
scope: vec![scope.to_string()],
..CwtFieldAttributes::default()
}),
value: CompiledRuleValue::Marker("float".to_string()),
rules: Vec::new(),
origin: CompiledRuleOrigin::Base,
}
}
fn from_alias(alias: &CwtAlias) -> Self {
Self {
category: CompiledAliasCategory::from_schema(&alias.category),
name: alias.name.clone(),
attributes: CompiledFieldAttributes::from_attributes(&alias.attributes),
value: CompiledRuleValue::from_rule_value(&alias.value),
rules: alias
.rules
.iter()
.map(CompiledRuleField::from_rule_field)
.collect(),
origin: CompiledRuleOrigin::from_schema(alias.origin),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum CompiledAliasCategory {
Trigger,
Effect,
Modifier,
Link,
Other(String),
}
impl CompiledAliasCategory {
pub fn from_name(name: &str) -> Self {
match name {
"trigger" => Self::Trigger,
"effect" => Self::Effect,
"modifier" => Self::Modifier,
"link" => Self::Link,
other => Self::Other(other.to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Trigger => "trigger",
Self::Effect => "effect",
Self::Modifier => "modifier",
Self::Link => "link",
Self::Other(name) => name.as_str(),
}
}
fn from_schema(category: &AliasCategory) -> Self {
match category {
AliasCategory::Trigger => Self::Trigger,
AliasCategory::Effect => Self::Effect,
AliasCategory::Modifier => Self::Modifier,
AliasCategory::Link => Self::Link,
AliasCategory::Other(name) => Self::Other(name.clone()),
}
}
}
impl Display for CompiledAliasCategory {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledFieldAttributes {
pub push_scope: Option<String>,
pub replace_scope: BTreeMap<String, String>,
pub scope: Vec<String>,
pub cardinality: Option<(u32, Option<u32>)>,
pub severity: Option<CompiledSeverity>,
pub description: Option<String>,
pub raw: Vec<(String, String)>,
}
impl CompiledFieldAttributes {
fn from_attributes(attributes: &CwtFieldAttributes) -> Self {
Self {
push_scope: attributes.push_scope.clone(),
replace_scope: attributes.replace_scope.clone(),
scope: attributes.scope.clone(),
cardinality: attributes.cardinality,
severity: attributes.severity.map(CompiledSeverity::from_schema),
description: attributes.description.clone(),
raw: attributes.raw.clone(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CompiledSeverity {
Error,
Warning,
Info,
}
impl CompiledSeverity {
fn from_schema(severity: CwtSeverity) -> Self {
match severity {
CwtSeverity::Error => Self::Error,
CwtSeverity::Warning => Self::Warning,
CwtSeverity::Info => Self::Info,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledRuleField {
pub key: String,
pub value: CompiledRuleValue,
pub attributes: CompiledFieldAttributes,
pub conditions: Vec<CompiledRuleCondition>,
pub origin: CompiledRuleOrigin,
}
impl CompiledRuleField {
fn from_rule_field(field: &CwtRuleField) -> Self {
Self {
key: field.key.clone(),
value: CompiledRuleValue::from_rule_value(&field.value),
attributes: CompiledFieldAttributes::from_attributes(&field.attributes),
conditions: field
.conditions
.iter()
.map(CompiledRuleCondition::from_schema)
.collect(),
origin: CompiledRuleOrigin::from_schema(field.origin),
}
}
pub fn states_same_rule(&self, other: &Self) -> bool {
self.key == other.key
&& self.attributes == other.attributes
&& self.conditions == other.conditions
&& self.value.states_same_rule(&other.value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CompiledRuleOrigin {
Base,
Extension,
}
impl CompiledRuleOrigin {
fn from_schema(origin: CwtRuleOrigin) -> Self {
match origin {
CwtRuleOrigin::Base => Self::Base,
CwtRuleOrigin::Extension => Self::Extension,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CompiledRuleCondition {
SubtypeActive(String),
SubtypeInactive(String),
}
impl CompiledRuleCondition {
fn from_schema(condition: &CwtRuleCondition) -> Self {
match condition {
CwtRuleCondition::SubtypeActive(label) => Self::SubtypeActive(label.clone()),
CwtRuleCondition::SubtypeInactive(label) => Self::SubtypeInactive(label.clone()),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CompiledRuleValue {
Scalar(String),
Block(Vec<CompiledRuleField>),
Marker(String),
}
impl CompiledRuleValue {
fn from_rule_value(value: &CwtRuleValue) -> Self {
match value {
CwtRuleValue::Scalar(value) => Self::Scalar(value.clone()),
CwtRuleValue::Block(fields) => Self::Block(
fields
.iter()
.map(CompiledRuleField::from_rule_field)
.collect(),
),
CwtRuleValue::Marker(value) => Self::Marker(value.clone()),
}
}
pub fn states_same_rule(&self, other: &Self) -> bool {
match (self, other) {
(Self::Scalar(left), Self::Scalar(right))
| (Self::Marker(left), Self::Marker(right)) => left == right,
(Self::Block(left), Self::Block(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right)
.all(|(left, right)| left.states_same_rule(right))
}
_ => false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledStringSet {
pub name: String,
pub values: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CompiledScope {
pub name: String,
pub aliases: Vec<String>,
pub is_subscope_of: Vec<String>,
}
pub struct CwtQuery {
pack: Arc<CompiledRulePack>,
index: RuntimeRuleIndex,
}
impl CwtQuery {
pub fn from_graph(graph: &CwtSchemaGraph) -> Self {
Self::new(CompiledRulePack::from_graph(graph))
}
pub fn new(pack: CompiledRulePack) -> Self {
let pack = Arc::new(pack);
Self::from_arc(pack)
}
pub fn from_arc(pack: Arc<CompiledRulePack>) -> Self {
let index = RuntimeRuleIndex::new(pack.as_ref());
Self { pack, index }
}
pub fn pack(&self) -> &CompiledRulePack {
self.pack.as_ref()
}
pub fn root_count(&self) -> usize {
self.pack.roots.len()
}
pub fn alias_count(&self) -> usize {
self.pack.aliases.len()
}
pub fn aliases(&self) -> &[CompiledAlias] {
self.pack.aliases.as_slice()
}
pub fn root(&self, name: &str) -> Option<&CompiledRoot> {
self.index
.roots
.get(name)
.map(|index| &self.pack.roots[*index])
}
pub fn alias(&self, category: CompiledAliasCategory, name: &str) -> Option<&CompiledAlias> {
self.alias_overloads(category, name).next_back()
}
pub fn alias_overloads(
&self,
category: CompiledAliasCategory,
name: &str,
) -> impl DoubleEndedIterator<Item = &CompiledAlias> {
self.index
.aliases
.get(&category)
.and_then(|aliases| aliases.get(name))
.into_iter()
.flatten()
.map(|index| &self.pack.aliases[*index])
}
pub fn declared_modifier(&self, name: &str) -> Option<&CompiledAlias> {
self.index
.declared_modifiers
.get(name)
.map(|index| &self.pack.declared_modifiers[*index])
}
pub fn declared_modifiers(&self) -> &[CompiledAlias] {
self.pack.declared_modifiers.as_slice()
}
pub fn enum_values(&self, name: &str) -> Option<&[String]> {
self.index
.enums
.get(name)
.map(|index| self.pack.enums[*index].values.as_slice())
}
pub fn value_set_values(&self, name: &str) -> Option<&[String]> {
self.index
.value_sets
.get(name)
.map(|index| self.pack.value_sets[*index].values.as_slice())
}
pub fn complex_enum(&self, name: &str) -> Option<&CompiledComplexEnum> {
self.index
.complex_enums
.get(name)
.map(|index| &self.pack.complex_enums[*index])
}
pub fn complex_enums(&self) -> &[CompiledComplexEnum] {
self.pack.complex_enums.as_slice()
}
pub fn link(&self, name: &str) -> Option<&CompiledLink> {
self.index
.links
.get(name)
.map(|index| &self.pack.links[*index])
}
pub fn links(&self) -> &[CompiledLink] {
self.pack.links.as_slice()
}
pub fn scope_matches(&self, required_scope: &str, active_scope: &str) -> bool {
if required_scope == active_scope || required_scope == "any" || active_scope == "any" {
return true;
}
let Some(required_index) = self.index.scope_labels.get(required_scope).copied() else {
return false;
};
let Some(active_index) = self.index.scope_labels.get(active_scope).copied() else {
return false;
};
self.scope_index_matches(required_index, active_index)
}
pub fn is_unlisted_scope(&self, label: &str) -> bool {
label != "any"
&& !self.index.scope_labels.is_empty()
&& !self.index.scope_labels.contains_key(label)
}
pub fn scope_values(&self, required_scope: &str) -> Vec<String> {
let mut values = self
.pack
.scope_definitions
.iter()
.filter(|scope| {
required_scope == "any"
|| self.scope_matches(required_scope, &scope.name)
|| scope
.aliases
.iter()
.any(|alias| self.scope_matches(required_scope, alias))
})
.flat_map(|scope| scope.aliases.iter().cloned())
.collect::<Vec<_>>();
values.sort();
values.dedup();
values
}
pub fn bind_root(&self, file_path: &GamePath) -> Option<&CompiledRoot> {
let SchemaBinding::Bound { type_id, .. } = self.root_binding(file_path) else {
return None;
};
self.root(type_id.as_str())
}
pub fn root_binding(&self, file_path: &GamePath) -> SchemaBinding {
match self.matching_root_indices(file_path).as_slice() {
[] => unbound_root(file_path),
[index] => bound_node(&self.pack.roots[*index], Vec::new()),
_ => SchemaBinding::Dynamic {
reason: "ambiguous-root-type",
},
}
}
pub fn matching_roots(&self, file_path: &GamePath) -> Vec<&CompiledRoot> {
self.matching_root_indices(file_path)
.into_iter()
.map(|index| &self.pack.roots[index])
.collect()
}
pub fn bind_chain(&self, file_path: &GamePath, ast_path: &[&str]) -> SchemaBinding {
if ast_path.is_empty() {
return self.root_binding(file_path);
}
let matches = self.matching_root_indices(file_path);
if matches.is_empty() {
return unbound_root(file_path);
}
let attempts = matches
.into_iter()
.map(|index| self.bind_chain_from_root(&self.pack.roots[index], ast_path))
.collect::<Vec<_>>();
let max_consumed = attempts
.iter()
.map(|attempt| attempt.consumed)
.max()
.unwrap_or(0);
let best = attempts
.into_iter()
.filter(|attempt| attempt.consumed == max_consumed)
.collect::<Vec<_>>();
choose_best_binding(best, max_consumed)
}
pub fn bind_context<'p>(
&'p self,
file_path: &GamePath,
ast_path: &[&str],
) -> Option<RuleContext<'p>> {
let target = self.bind_chain(file_path, ast_path);
if !matches!(&target, SchemaBinding::Bound { .. }) {
return None;
}
for index in self.matching_root_indices(file_path) {
let root = &self.pack.roots[index];
let Some((context, binding)) = self.bind_context_from_root(root, ast_path) else {
continue;
};
if binding == target {
return Some(context);
}
}
None
}
pub fn bind_fields<'p>(
&'p self,
parent: RuleContext<'p>,
key: &str,
) -> Vec<&'p CompiledRuleField> {
let Some(rule_sets) = parent_rule_sets(parent) else {
return Vec::new();
};
let mut matches = self.match_rule_sets_all(&rule_sets, key);
if !matches.is_empty() {
return matches;
}
for rules in &rule_sets {
match self.match_alias_rules(rules, key) {
Some(RuleMatch::Alias { wildcard, .. }) => matches.push(wildcard),
Some(RuleMatch::Dynamic { .. }) => return Vec::new(),
_ => {}
}
}
if !matches.is_empty() {
return matches;
}
for rules in &rule_sets {
match self.match_dynamic_key_rules(rules, key, &every_field) {
Some(RuleMatch::Field(field)) => return vec![field],
Some(RuleMatch::Dynamic { .. }) => return Vec::new(),
_ => {}
}
}
matches
}
pub fn bind_field_match<'p>(
&'p self,
parent: RuleContext<'p>,
key: &str,
) -> Option<CompiledBindFieldMatch<'p>> {
self.bind_field_matches(parent, key).into_iter().next()
}
pub fn bind_field_matches<'p>(
&'p self,
parent: RuleContext<'p>,
key: &str,
) -> Vec<CompiledBindFieldMatch<'p>> {
self.bind_field_matches_with(parent, key, AliasOverloads::Bound, &every_field)
}
pub fn bind_field_overloads<'p>(
&'p self,
parent: RuleContext<'p>,
key: &str,
admits: impl Fn(&CompiledRuleField) -> bool,
) -> Vec<CompiledBindFieldMatch<'p>> {
self.bind_field_matches_with(parent, key, AliasOverloads::Every, &admits)
}
fn bind_field_matches_with<'p>(
&'p self,
parent: RuleContext<'p>,
key: &str,
overloads: AliasOverloads,
admits: &dyn Fn(&CompiledRuleField) -> bool,
) -> Vec<CompiledBindFieldMatch<'p>> {
let Some(rule_sets) = parent_rule_sets(parent) else {
return Vec::new();
};
let mut field_matches = Vec::new();
for rules in &rule_sets {
field_matches.extend(
rules
.iter()
.filter(|field| field.key == key && admits(field))
.map(CompiledBindFieldMatch::Field),
);
}
if !field_matches.is_empty() {
return field_matches;
}
for rules in &rule_sets {
field_matches.extend(
self.match_alias_rule_fields(rules, key, overloads)
.into_iter()
.filter(|field_match| admits(field_match.field())),
);
}
if !field_matches.is_empty() {
return field_matches;
}
for rules in &rule_sets {
match self.match_dynamic_key_rules(rules, key, admits) {
Some(RuleMatch::Field(field)) => {
return vec![CompiledBindFieldMatch::Field(field)];
}
Some(RuleMatch::Dynamic { .. }) => return Vec::new(),
_ => {}
}
}
Vec::new()
}
pub fn bind_field<'p>(
&'p self,
parent: RuleContext<'p>,
key: &str,
) -> Option<&'p CompiledRuleField> {
self.bind_field_match(parent, key)
.map(|field_match| field_match.field())
}
fn matching_root_indices(&self, file_path: &GamePath) -> Vec<usize> {
let mut matches = self
.pack
.roots
.iter()
.enumerate()
.filter_map(|(index, root)| {
root.match_depth(file_path)
.map(|depth| (depth, root.name.as_str(), index))
})
.collect::<Vec<_>>();
matches.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(right.1)));
let Some((best_length, _, _)) = matches.first() else {
return Vec::new();
};
let best_length = *best_length;
matches
.into_iter()
.filter(|(length, _, _)| *length == best_length)
.map(|(_, _, index)| index)
.collect()
}
fn scope_index_matches(&self, required_index: usize, active_index: usize) -> bool {
if required_index == active_index {
return true;
}
let mut stack = vec![active_index];
let mut visited = vec![false; self.pack.scope_definitions.len()];
while let Some(index) = stack.pop() {
let Some(visited_entry) = visited.get_mut(index) else {
continue;
};
if *visited_entry {
continue;
}
*visited_entry = true;
if index == required_index {
return true;
}
let Some(scope) = self.pack.scope_definitions.get(index) else {
continue;
};
for parent in &scope.is_subscope_of {
if let Some(parent_index) = self.index.scope_labels.get(parent).copied() {
stack.push(parent_index);
}
}
}
false
}
fn scope_value_matches(&self, required_scope: &str, value: &str) -> bool {
self.pack.scope_definitions.iter().any(|scope| {
scope.aliases.iter().any(|alias| alias == value)
&& (required_scope == "any"
|| self.scope_matches(required_scope, &scope.name)
|| scope
.aliases
.iter()
.any(|alias| self.scope_matches(required_scope, alias)))
})
}
fn bind_chain_from_root(&self, root: &CompiledRoot, ast_path: &[&str]) -> BindAttempt {
let mut node = ResolvedNode::Root(root);
let mut node_path = Vec::new();
let mut consumed = 0;
let mut root_instance_consumed = false;
let mut skip_root_index = 0;
let mut segments = ast_path.iter().copied().peekable();
while let Some(key) = segments.next() {
if let ResolvedNode::Root(definition) = node {
if let Some(skip_key) = definition.skip_root_keys.get(skip_root_index) {
if root_skip_key_matches(skip_key, key) {
skip_root_index += 1;
consumed += 1;
continue;
}
return BindAttempt {
consumed,
binding: SchemaBinding::Unbound {
reason: format!(
"expected schema skip_root_key `{skip_key}` under {}",
describe_node(node)
),
},
};
}
match self.match_subtype(definition, key) {
Some(Ok(subtype)) => {
node = ResolvedNode::Subtype(definition, subtype);
node_path.push(format!("subtype:{}", subtype_label(subtype)));
consumed += 1;
continue;
}
Some(Err(reason)) => {
return BindAttempt {
consumed,
binding: SchemaBinding::Dynamic { reason },
};
}
None => {}
}
if !definition.skip_root_keys.is_empty() && !root_instance_consumed {
if root_type_accepts_instance_key(definition, key) {
root_instance_consumed = true;
consumed += 1;
if segments.peek().is_none() {
return BindAttempt {
consumed,
binding: bound_node(root, node_path),
};
}
continue;
}
return BindAttempt {
consumed,
binding: SchemaBinding::Unbound {
reason: format!(
"expected root instance key under {}",
describe_node(node)
),
},
};
}
}
if let Some(rule_match) = self.match_rules_for_node(node, key) {
match rule_match {
RuleMatch::Field(field) => {
node = ResolvedNode::Field(field);
node_path.push(format!("field:{}", field.key));
consumed += 1;
continue;
}
RuleMatch::Alias { alias, .. } => {
node = ResolvedNode::Alias(alias);
node_path.push(format!("alias:{}:{}", alias.category, alias.name));
consumed += 1;
continue;
}
RuleMatch::Dynamic { reason } => {
return BindAttempt {
consumed,
binding: SchemaBinding::Dynamic { reason },
};
}
}
}
if let ResolvedNode::Root(definition) = node
&& !root_instance_consumed
&& root_type_accepts_instance_key(definition, key)
{
root_instance_consumed = true;
consumed += 1;
if segments.peek().is_none() {
return BindAttempt {
consumed,
binding: bound_node(root, node_path),
};
}
continue;
}
return BindAttempt {
consumed,
binding: SchemaBinding::Unbound {
reason: format!(
"no schema field matches `{key}` under {}",
describe_node(node)
),
},
};
}
BindAttempt {
consumed,
binding: bound_node(root, node_path),
}
}
fn bind_context_from_root<'p>(
&'p self,
root: &'p CompiledRoot,
ast_path: &[&str],
) -> Option<(RuleContext<'p>, SchemaBinding)> {
let mut node = ResolvedNode::Root(root);
let mut node_path = Vec::new();
let mut root_instance_consumed = false;
let mut skip_root_index = 0;
let mut segments = ast_path.iter().copied().peekable();
while let Some(key) = segments.next() {
if let ResolvedNode::Root(definition) = node {
if let Some(skip_key) = definition.skip_root_keys.get(skip_root_index) {
if root_skip_key_matches(skip_key, key) {
skip_root_index += 1;
continue;
}
return None;
}
match self.match_subtype(definition, key) {
Some(Ok(subtype)) => {
node = ResolvedNode::Subtype(definition, subtype);
node_path.push(format!("subtype:{}", subtype_label(subtype)));
continue;
}
Some(Err(_)) => return None,
None => {}
}
if !definition.skip_root_keys.is_empty() && !root_instance_consumed {
if root_type_accepts_instance_key(definition, key) {
root_instance_consumed = true;
if segments.peek().is_none() {
return Some((
RuleContext::RootType(root),
bound_node(root, node_path),
));
}
continue;
}
return None;
}
}
if let Some(rule_match) = self.match_rules_for_node(node, key) {
match rule_match {
RuleMatch::Field(field) => {
node = ResolvedNode::Field(field);
node_path.push(format!("field:{}", field.key));
continue;
}
RuleMatch::Alias { alias, .. } => {
node = ResolvedNode::Alias(alias);
node_path.push(format!("alias:{}:{}", alias.category, alias.name));
continue;
}
RuleMatch::Dynamic { .. } => return None,
}
}
if let ResolvedNode::Root(definition) = node
&& !root_instance_consumed
&& root_type_accepts_instance_key(definition, key)
{
root_instance_consumed = true;
if segments.peek().is_none() {
return Some((RuleContext::RootType(root), bound_node(root, node_path)));
}
continue;
}
return None;
}
Some((context_for_node(node), bound_node(root, node_path)))
}
fn match_rules_for_node<'p>(
&'p self,
node: ResolvedNode<'p>,
key: &str,
) -> Option<RuleMatch<'p>> {
match node {
ResolvedNode::Root(definition) => {
self.match_rule_sets(&[definition.rules.as_slice()], key)
}
ResolvedNode::Subtype(definition, subtype) => self.match_rule_sets(
&[subtype.rules.as_slice(), definition.rules.as_slice()],
key,
),
ResolvedNode::Field(field) => self.match_rule_sets(&[field_rules(field)?], key),
ResolvedNode::Alias(alias) => self.match_rule_sets(&[alias.rules.as_slice()], key),
}
}
fn match_rule_sets<'p>(
&'p self,
rule_sets: &[&'p [CompiledRuleField]],
key: &str,
) -> Option<RuleMatch<'p>> {
for rules in rule_sets {
if let Some(field) = rules.iter().find(|field| field.key == key) {
return Some(RuleMatch::Field(field));
}
}
for rules in rule_sets {
if let Some(alias_match) = self.match_alias_rules(rules, key) {
return Some(alias_match);
}
}
for rules in rule_sets {
if let Some(dynamic_match) = self.match_dynamic_key_rules(rules, key, &every_field) {
return Some(dynamic_match);
}
}
None
}
fn match_rule_sets_all<'p>(
&'p self,
rule_sets: &[&'p [CompiledRuleField]],
key: &str,
) -> Vec<&'p CompiledRuleField> {
let mut matches = Vec::new();
for rules in rule_sets {
matches.extend(rules.iter().filter(|field| field.key == key));
}
matches
}
fn match_alias_rules<'p>(
&'p self,
rules: &'p [CompiledRuleField],
key: &str,
) -> Option<RuleMatch<'p>> {
let matches = self.match_alias_rule_fields(rules, key, AliasOverloads::Bound);
match matches.as_slice() {
[] => None,
[CompiledBindFieldMatch::Alias { wildcard, alias }] => {
Some(RuleMatch::Alias { wildcard, alias })
}
_ => Some(RuleMatch::Dynamic {
reason: "ambiguous-alias-match",
}),
}
}
fn match_alias_rule_fields<'p>(
&'p self,
rules: &'p [CompiledRuleField],
key: &str,
overloads: AliasOverloads,
) -> Vec<CompiledBindFieldMatch<'p>> {
let mut matches = Vec::new();
for field in rules {
let Some((head, payload)) = parse_marker(&field.key) else {
continue;
};
if head != "alias_name" {
continue;
}
let category = CompiledAliasCategory::from_name(payload);
let mut definitions = self.alias_overloads(category.clone(), key);
let to_match = |alias| CompiledBindFieldMatch::Alias {
wildcard: field,
alias,
};
match overloads {
AliasOverloads::Bound => matches.extend(definitions.next_back().map(to_match)),
AliasOverloads::Every => {
matches.extend(definitions.rev().map(to_match));
if category == CompiledAliasCategory::Modifier {
matches.extend(self.declared_modifier(key).map(to_match));
}
}
}
}
matches
}
fn match_dynamic_key_rules<'p>(
&'p self,
rules: &'p [CompiledRuleField],
key: &str,
admits: &dyn Fn(&CompiledRuleField) -> bool,
) -> Option<RuleMatch<'p>> {
if is_dynamic_key_marker(key) {
return None;
}
let matches = rules
.iter()
.filter(|field| admits(field) && self.dynamic_key_rule_matches(&field.key, key))
.collect::<Vec<_>>();
match matches.as_slice() {
[] => None,
[field] => Some(RuleMatch::Field(field)),
_ => Some(RuleMatch::Dynamic {
reason: "ambiguous-dynamic-field-match",
}),
}
}
fn dynamic_key_rule_matches(&self, rule_key: &str, key: &str) -> bool {
if is_angle_dynamic_key_marker(rule_key) {
return true;
}
let Some((head, payload)) = parse_marker(rule_key) else {
return false;
};
match head {
"enum" => self
.enum_values(payload)
.is_some_and(|values| values.iter().any(|value| value == key)),
"value" | "value_set" => self
.value_set_values(payload)
.is_some_and(|values| values.iter().any(|value| value == key)),
"scope" => self.scope_value_matches(payload, key),
_ => false,
}
}
fn match_subtype<'p>(
&'p self,
definition: &'p CompiledRoot,
key: &str,
) -> Option<Result<&'p CompiledSubtype, &'static str>> {
let matches = definition
.subtypes
.iter()
.filter(|subtype| subtype_matches(subtype, key))
.collect::<Vec<_>>();
match matches.as_slice() {
[] => None,
[subtype] => Some(Ok(*subtype)),
_ => Some(Err("ambiguous-subtype")),
}
}
}
#[derive(Clone, Copy, Debug)]
enum AliasOverloads {
Bound,
Every,
}
#[derive(Clone, Copy, Debug)]
pub enum RuleContext<'p> {
RootType(&'p CompiledRoot),
Subtype(&'p CompiledRoot, &'p CompiledSubtype),
RuleField(&'p CompiledRuleField),
AliasRules(&'p [CompiledRuleField]),
}
#[derive(Clone, Copy, Debug)]
pub enum CompiledBindFieldMatch<'p> {
Field(&'p CompiledRuleField),
Alias {
wildcard: &'p CompiledRuleField,
alias: &'p CompiledAlias,
},
}
impl<'p> CompiledBindFieldMatch<'p> {
pub fn field(&self) -> &'p CompiledRuleField {
match self {
Self::Field(field)
| Self::Alias {
wildcard: field, ..
} => field,
}
}
pub fn alias(&self) -> Option<&'p CompiledAlias> {
match self {
Self::Field(_) => None,
Self::Alias { alias, .. } => Some(alias),
}
}
pub fn value(&self) -> &'p CompiledRuleValue {
match self {
Self::Field(field) => &field.value,
Self::Alias { alias, .. } => &alias.value,
}
}
pub fn origin(&self) -> CompiledRuleOrigin {
match self {
Self::Field(field) => field.origin,
Self::Alias { alias, .. } => alias.origin,
}
}
}
struct RuntimeRuleIndex {
roots: HashMap<String, usize>,
aliases: HashMap<CompiledAliasCategory, HashMap<String, Vec<usize>>>,
declared_modifiers: HashMap<String, usize>,
enums: HashMap<String, usize>,
value_sets: HashMap<String, usize>,
complex_enums: HashMap<String, usize>,
links: HashMap<String, usize>,
scope_labels: HashMap<String, usize>,
}
impl RuntimeRuleIndex {
fn new(pack: &CompiledRulePack) -> Self {
let roots = pack
.roots
.iter()
.enumerate()
.map(|(index, root)| (root.name.clone(), index))
.collect();
let mut aliases = HashMap::<CompiledAliasCategory, HashMap<String, Vec<usize>>>::new();
for (index, alias) in pack.aliases.iter().enumerate() {
aliases
.entry(alias.category.clone())
.or_default()
.entry(alias.name.clone())
.or_default()
.push(index);
}
let declared_modifiers = pack
.declared_modifiers
.iter()
.enumerate()
.map(|(index, modifier)| (modifier.name.clone(), index))
.collect();
let enums = pack
.enums
.iter()
.enumerate()
.map(|(index, values)| (values.name.clone(), index))
.collect();
let value_sets = pack
.value_sets
.iter()
.enumerate()
.map(|(index, values)| (values.name.clone(), index))
.collect();
let complex_enums = pack
.complex_enums
.iter()
.enumerate()
.map(|(index, complex_enum)| (complex_enum.name.clone(), index))
.collect();
let links = pack
.links
.iter()
.enumerate()
.map(|(index, link)| (link.name.clone(), index))
.collect();
let mut scope_labels = HashMap::new();
for (index, scope) in pack.scope_definitions.iter().enumerate() {
scope_labels.entry(scope.name.clone()).or_insert(index);
for alias in &scope.aliases {
scope_labels.entry(alias.clone()).or_insert(index);
}
}
Self {
roots,
aliases,
declared_modifiers,
enums,
value_sets,
complex_enums,
links,
scope_labels,
}
}
}
#[derive(Clone, Copy)]
enum ResolvedNode<'p> {
Root(&'p CompiledRoot),
Subtype(&'p CompiledRoot, &'p CompiledSubtype),
Field(&'p CompiledRuleField),
Alias(&'p CompiledAlias),
}
enum RuleMatch<'p> {
Field(&'p CompiledRuleField),
Alias {
wildcard: &'p CompiledRuleField,
alias: &'p CompiledAlias,
},
Dynamic {
reason: &'static str,
},
}
#[derive(Clone)]
struct BindAttempt {
consumed: usize,
binding: SchemaBinding,
}
fn choose_best_binding(attempts: Vec<BindAttempt>, max_consumed: usize) -> SchemaBinding {
let Some(first) = attempts.first() else {
return SchemaBinding::Dynamic {
reason: "ambiguous-binding",
};
};
if attempts
.iter()
.all(|attempt| attempt.binding == first.binding)
{
return first.binding.clone();
}
let bound = attempts
.iter()
.filter(|attempt| matches!(attempt.binding, SchemaBinding::Bound { .. }))
.collect::<Vec<_>>();
if bound.len() == 1 {
return bound[0].binding.clone();
}
let dynamic = attempts
.iter()
.filter(|attempt| matches!(attempt.binding, SchemaBinding::Dynamic { .. }))
.collect::<Vec<_>>();
if bound.is_empty() && dynamic.len() == 1 {
return dynamic[0].binding.clone();
}
if max_consumed == 0 {
SchemaBinding::Dynamic {
reason: "ambiguous-root-type",
}
} else {
SchemaBinding::Dynamic {
reason: "ambiguous-binding",
}
}
}
fn every_field(_: &CompiledRuleField) -> bool {
true
}
fn parent_rule_sets(parent: RuleContext<'_>) -> Option<Vec<&[CompiledRuleField]>> {
match parent {
RuleContext::RootType(root) => Some(vec![root.rules.as_slice()]),
RuleContext::Subtype(root, subtype) => {
Some(vec![subtype.rules.as_slice(), root.rules.as_slice()])
}
RuleContext::RuleField(field) => Some(vec![field_rules(field)?]),
RuleContext::AliasRules(rules) => Some(vec![rules]),
}
}
fn context_for_node(node: ResolvedNode<'_>) -> RuleContext<'_> {
match node {
ResolvedNode::Root(root) => RuleContext::RootType(root),
ResolvedNode::Subtype(root, subtype) => RuleContext::Subtype(root, subtype),
ResolvedNode::Field(field) => RuleContext::RuleField(field),
ResolvedNode::Alias(alias) => RuleContext::AliasRules(alias.rules.as_slice()),
}
}
fn field_rules(field: &CompiledRuleField) -> Option<&[CompiledRuleField]> {
let CompiledRuleValue::Block(fields) = &field.value else {
return None;
};
Some(fields.as_slice())
}
fn unbound_root(file_path: &GamePath) -> SchemaBinding {
SchemaBinding::Unbound {
reason: format!(
"no root type matches `{}`",
file_path.as_str().to_ascii_lowercase()
),
}
}
fn root_skip_key_matches(skip_key: &str, key: &str) -> bool {
skip_key == "any" || skip_key == key
}
fn is_dynamic_key_marker(key: &str) -> bool {
is_angle_dynamic_key_marker(key)
|| matches!(
parse_marker(key),
Some(("enum" | "value" | "value_set" | "scope", _))
)
}
fn is_angle_dynamic_key_marker(key: &str) -> bool {
key.len() > 2
&& key.starts_with('<')
&& key.ends_with('>')
&& !key.chars().any(char::is_whitespace)
}
fn subtype_matches(subtype: &CompiledSubtype, key: &str) -> bool {
subtype.name == key
|| subtype
.type_key_filter
.as_ref()
.is_some_and(|filter| filter.matches(key))
}
fn subtype_label(subtype: &CompiledSubtype) -> &str {
subtype
.type_key_filter
.as_ref()
.and_then(|filter| filter.primary_label())
.unwrap_or(&subtype.name)
}
fn root_type_accepts_instance_key(definition: &CompiledRoot, key: &str) -> bool {
definition.type_key_filter.as_ref().map_or_else(
|| definition.subtypes.is_empty(),
|filter| filter.matches(key),
)
}
fn describe_node(node: ResolvedNode<'_>) -> String {
match node {
ResolvedNode::Root(definition) => format!("root type `{}`", definition.name),
ResolvedNode::Subtype(definition, subtype) => format!(
"subtype `{}` of type `{}`",
subtype_label(subtype),
definition.name
),
ResolvedNode::Field(field) => format!("field `{}`", field.key),
ResolvedNode::Alias(alias) => {
format!("alias `{}` in category `{}`", alias.name, alias.category)
}
}
}
fn bound_node(definition: &CompiledRoot, path: Vec<String>) -> SchemaBinding {
let node_id = if path.is_empty() {
CwtNodeId(format!("type:{}:root", definition.name))
} else {
CwtNodeId(format!("type:{}:{}", definition.name, path.join("/")))
};
SchemaBinding::Bound {
type_id: SchemaRootId::new(definition.name.clone()),
node_id,
}
}
fn parse_marker(text: &str) -> Option<(&str, &str)> {
let (head, rest) = text.split_once('[')?;
Some((head, rest.strip_suffix(']')?))
}
fn sorted_string_sets(map: &HashMap<String, Vec<String>>) -> Vec<CompiledStringSet> {
let mut sets = map
.iter()
.map(|(name, values)| {
let mut values = values.clone();
values.sort();
values.dedup();
CompiledStringSet {
name: name.clone(),
values,
}
})
.collect::<Vec<_>>();
sets.sort_by(|left, right| left.name.cmp(&right.name));
sets
}
fn sorted_complex_enums(map: &HashMap<String, CwtComplexEnum>) -> Vec<CompiledComplexEnum> {
let mut complex_enums = map
.values()
.map(CompiledComplexEnum::from_complex_enum)
.collect::<Vec<_>>();
complex_enums.sort_by(|left, right| left.name.cmp(&right.name));
complex_enums
}
fn sorted_links(map: &HashMap<String, CwtLink>) -> Vec<CompiledLink> {
let mut links = map
.values()
.map(CompiledLink::from_link)
.collect::<Vec<_>>();
links.sort_by(|left, right| left.name.cmp(&right.name));
links
}
fn sorted_scope_definitions(definitions: &[CwtScope]) -> Vec<CompiledScope> {
let mut by_name = BTreeMap::<String, CompiledScope>::new();
for definition in definitions {
let scope = by_name
.entry(definition.name.clone())
.or_insert_with(|| CompiledScope {
name: definition.name.clone(),
aliases: Vec::new(),
is_subscope_of: Vec::new(),
});
scope.aliases.extend(definition.aliases.iter().cloned());
scope
.is_subscope_of
.extend(definition.is_subscope_of.iter().cloned());
}
for scope in by_name.values_mut() {
scope.aliases.sort();
scope.aliases.dedup();
scope.is_subscope_of.sort();
scope.is_subscope_of.dedup();
}
by_name.into_values().collect()
}
#[cfg(test)]
mod tests {
use super::super::rule_path::SchemaDirectory;
use crate::model::GamePath;
#[test]
fn root_path_matching_requires_a_component_boundary() {
let file = |text| GamePath::new(text).expect("valid game path");
let common = SchemaDirectory::parse("game/common").expect("valid rule path");
assert_eq!(common.match_depth(None, file("common/foo")), Some(1));
assert_eq!(common.match_depth(None, file("common")), Some(1));
assert_eq!(common.match_depth(None, file("commonplace/foo")), None);
assert_eq!(
SchemaDirectory::GameRoot.match_depth(None, file("events/a.txt")),
None
);
}
}
pub fn parse_schema_marker(text: &str) -> Option<(&str, &str)> {
let (head, rest) = text.split_once('[')?;
Some((head, rest.strip_suffix(']')?))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SchemaScalarType {
Int { range: Option<SchemaIntRange> },
Float { range: Option<SchemaFloatRange> },
Bool,
}
impl SchemaScalarType {
pub fn from_rule_value(value: &CompiledRuleValue) -> Option<Self> {
let value = match value {
CompiledRuleValue::Scalar(value) | CompiledRuleValue::Marker(value) => value.as_str(),
CompiledRuleValue::Block(_) => return None,
};
match value {
"int" => Some(Self::Int { range: None }),
"float" => Some(Self::Float { range: None }),
"bool" => Some(Self::Bool),
_ => match parse_schema_marker(value) {
Some(("int", range)) => {
parse_schema_int_range(range).map(|range| Self::Int { range: Some(range) })
}
Some(("float", range)) => {
parse_schema_float_range(range).map(|range| Self::Float { range: Some(range) })
}
_ => None,
},
}
}
pub fn is_same_primitive(self, other: Self) -> bool {
matches!(
(self, other),
(Self::Int { .. }, Self::Int { .. })
| (Self::Float { .. }, Self::Float { .. })
| (Self::Bool, Self::Bool)
)
}
pub fn label(&self) -> String {
match self {
Self::Int { range: None } => "int".to_string(),
Self::Int { range: Some(range) } => range.label("int"),
Self::Float { range: None } => "float".to_string(),
Self::Float { range: Some(range) } => range.label("float"),
Self::Bool => "bool".to_string(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SchemaIntRange {
pub minimum: i64,
pub maximum: Option<i64>,
}
impl SchemaIntRange {
pub fn contains(self, value: i64) -> bool {
value >= self.minimum && self.maximum.is_none_or(|maximum| value <= maximum)
}
pub fn label(self, kind: &str) -> String {
format!(
"{kind}[{}..{}]",
self.minimum,
self.maximum
.map(|value| value.to_string())
.unwrap_or_else(|| "inf".to_string())
)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SchemaFloatRange {
pub minimum: f64,
pub maximum: Option<f64>,
}
impl SchemaFloatRange {
pub fn contains(self, value: f64) -> bool {
value >= self.minimum && self.maximum.is_none_or(|maximum| value <= maximum)
}
pub fn label(self, kind: &str) -> String {
format!(
"{kind}[{}..{}]",
self.minimum,
self.maximum
.map(|value| value.to_string())
.unwrap_or_else(|| "inf".to_string())
)
}
}
fn parse_schema_int_range(value: &str) -> Option<SchemaIntRange> {
let (minimum, maximum) = value.split_once("..")?;
let minimum = minimum.trim().parse::<i64>().ok()?;
let maximum = match maximum.trim() {
"inf" => None,
value => Some(value.parse::<i64>().ok()?),
};
Some(SchemaIntRange { minimum, maximum })
}
fn parse_schema_float_range(value: &str) -> Option<SchemaFloatRange> {
let (minimum, maximum) = value.split_once("..")?;
let minimum = minimum.trim().parse::<f64>().ok()?;
let maximum = match maximum.trim() {
"inf" => None,
value => Some(value.parse::<f64>().ok()?),
};
Some(SchemaFloatRange { minimum, maximum })
}