use super::types::TypeSchema;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Schema {
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub irs_form: Option<IrsFormMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pdf_template: Option<PdfTemplate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub import: Option<ImportConfig>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub definitions: IndexMap<String, TypeSchema>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub sections: BTreeMap<String, SectionDefinition>,
pub root: TypeSchema,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IrsFormMetadata {
pub name: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision_date: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub omb_number: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cat_number: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PdfTemplate {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_uri: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImportConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_rows: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_file_size: Option<usize>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub column_mappings: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_columns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Default for ImportConfig {
fn default() -> Self {
Self {
enabled: false,
max_rows: None,
max_file_size: Some(50 * 1024 * 1024), column_mappings: BTreeMap::new(),
required_columns: Vec::new(),
description: None,
}
}
}
impl ImportConfig {
pub fn enabled() -> Self {
Self {
enabled: true,
..Default::default()
}
}
pub fn with_max_rows(mut self, max: usize) -> Self {
self.max_rows = Some(max);
self
}
pub fn with_max_file_size(mut self, max: usize) -> Self {
self.max_file_size = Some(max);
self
}
pub fn with_column_mapping(
mut self,
field: impl Into<String>,
column: impl Into<String>,
) -> Self {
self.column_mappings.insert(field.into(), column.into());
self
}
pub fn with_required_columns(mut self, columns: Vec<String>) -> Self {
self.required_columns = columns;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SectionDefinition {
pub title: String,
pub order: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Schema {
pub fn new(version: impl Into<String>, root: TypeSchema) -> Self {
Self {
version: version.into(),
id: None,
title: None,
description: None,
irs_form: None,
pdf_template: None,
import: None,
definitions: IndexMap::new(),
sections: BTreeMap::new(),
root,
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn define(mut self, name: impl Into<String>, schema: TypeSchema) -> Self {
self.definitions.insert(name.into(), schema);
self
}
pub fn get_definition(&self, name: &str) -> Option<&TypeSchema> {
self.definitions.get(name)
}
pub fn resolve_ref(&self, name: &str) -> Option<&TypeSchema> {
self.definitions.get(name)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SchemaMeta {
pub id: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tax_year: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form_type: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::types::{ObjectSchema, StringSchema};
#[test]
fn test_schema_new() {
let schema = Schema::new("1.0.0", TypeSchema::object())
.with_id("test-schema")
.with_title("Test Schema");
assert_eq!(schema.version, "1.0.0");
assert_eq!(schema.id, Some("test-schema".to_string()));
assert_eq!(schema.title, Some("Test Schema".to_string()));
}
#[test]
fn test_schema_with_definitions() {
let schema = Schema::new("1.0.0", TypeSchema::ref_to("Person")).define(
"Person",
TypeSchema::Object(
ObjectSchema::new()
.property("name", TypeSchema::string())
.property("age", TypeSchema::integer()),
),
);
assert!(schema.get_definition("Person").is_some());
assert!(schema.get_definition("Unknown").is_none());
}
#[test]
fn test_schema_serde_roundtrip() {
let schema = Schema::new(
"1.0.0",
TypeSchema::Object(
ObjectSchema::new()
.property("tin", TypeSchema::String(StringSchema::new()))
.property("name", TypeSchema::string()),
),
)
.with_id("1099-nec-2025")
.with_title("Form 1099-NEC (2025)");
let json = serde_json::to_string_pretty(&schema).unwrap();
let parsed: Schema = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, schema);
}
#[test]
fn test_schema_json_format() {
let schema = Schema::new("1.0.0", TypeSchema::string());
let json = serde_json::to_string_pretty(&schema).unwrap();
assert!(json.contains("\"version\""));
assert!(json.contains("\"root\""));
}
}