Skip to main content

copybook_core/
schema.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Schema types for COBOL copybook structures
3//!
4//! This module defines the core data structures that represent a parsed
5//! COBOL copybook schema, including fields, types, and layout information.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// A parsed COBOL copybook schema
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Schema {
13    /// Root fields in the schema
14    pub fields: Vec<Field>,
15    /// Fixed record length (LRECL) if applicable
16    pub lrecl_fixed: Option<u32>,
17    /// Tail ODO information if present
18    pub tail_odo: Option<TailODO>,
19    /// Schema fingerprint for provenance tracking
20    pub fingerprint: String,
21}
22
23/// Information about tail ODO (OCCURS DEPENDING ON) arrays
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TailODO {
26    /// Path to the ODO counter field
27    pub counter_path: String,
28    /// Minimum array length
29    pub min_count: u32,
30    /// Maximum array length
31    pub max_count: u32,
32    /// Path to the ODO array field
33    pub array_path: String,
34}
35
36/// A field in the copybook schema
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Field {
39    /// Hierarchical path (e.g., "ROOT.CUSTOMER.ID")
40    pub path: String,
41    /// Field name (last component of path)
42    pub name: String,
43    /// Level number from copybook
44    pub level: u8,
45    /// Field type and characteristics
46    pub kind: FieldKind,
47    /// Byte offset within record
48    pub offset: u32,
49    /// Field length in bytes
50    pub len: u32,
51    /// Path of field this redefines (if any)
52    pub redefines_of: Option<String>,
53    /// Array information (if any)
54    pub occurs: Option<Occurs>,
55    /// Alignment padding bytes (if SYNCHRONIZED)
56    pub sync_padding: Option<u16>,
57    /// Whether field is SYNCHRONIZED
58    pub synchronized: bool,
59    /// Whether field has BLANK WHEN ZERO
60    pub blank_when_zero: bool,
61    /// Resolved RENAMES information (for level-66 fields only)
62    pub resolved_renames: Option<ResolvedRenames>,
63    /// Child fields (for groups)
64    pub children: Vec<Field>,
65}
66
67/// Field type and characteristics
68///
69/// # Examples
70///
71/// ```
72/// use copybook_core::FieldKind;
73///
74/// let alpha = FieldKind::Alphanum { len: 10 };
75/// assert!(matches!(alpha, FieldKind::Alphanum { len: 10 }));
76///
77/// let packed = FieldKind::PackedDecimal { digits: 7, scale: 2, signed: true };
78/// assert!(matches!(packed, FieldKind::PackedDecimal { signed: true, .. }));
79///
80/// let group = FieldKind::Group;
81/// assert!(matches!(group, FieldKind::Group));
82/// ```
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum FieldKind {
85    /// Alphanumeric field (PIC X)
86    Alphanum {
87        /// Field length in characters
88        len: u32,
89    },
90    /// Zoned decimal field (PIC 9, display)
91    ZonedDecimal {
92        /// Total number of digits
93        digits: u16,
94        /// Decimal places (can be negative for scaling)
95        scale: i16,
96        /// Whether field is signed
97        signed: bool,
98        /// SIGN SEPARATE clause information (if applicable)
99        sign_separate: Option<SignSeparateInfo>,
100    },
101    /// Binary integer field (COMP/BINARY)
102    BinaryInt {
103        /// Number of bits (16, 32, 64)
104        bits: u16,
105        /// Whether field is signed
106        signed: bool,
107    },
108    /// Packed decimal field (COMP-3)
109    PackedDecimal {
110        /// Total number of digits
111        digits: u16,
112        /// Decimal places (can be negative for scaling)
113        scale: i16,
114        /// Whether field is signed
115        signed: bool,
116    },
117    /// Group field (contains other fields)
118    Group,
119    /// Level-88 condition field (conditional values)
120    Condition {
121        /// Condition values (e.g., VALUE 'A', VALUE 1 THROUGH 5)
122        values: Vec<String>,
123    },
124    /// Level-66 RENAMES field (field aliasing/regrouping)
125    Renames {
126        /// Starting field name in the range
127        from_field: String,
128        /// Ending field name in the range
129        thru_field: String,
130    },
131    /// Edited numeric field (Phase E2: parse, represent, and decode)
132    /// Examples: PIC ZZZ9, PIC $ZZ,ZZ9.99, PIC 9(7)V99CR
133    EditedNumeric {
134        /// Original PIC string (e.g., "ZZ,ZZZ.99")
135        pic_string: String,
136        /// Display width (computed from PIC)
137        width: u16,
138        /// Decimal places (scale) for numeric value
139        scale: u16,
140        /// Whether field has sign editing
141        signed: bool,
142    },
143    /// Single-precision floating-point (COMP-1, IEEE 754 binary32, 4 bytes)
144    FloatSingle,
145    /// Double-precision floating-point (COMP-2, IEEE 754 binary64, 8 bytes)
146    FloatDouble,
147}
148
149/// Resolved RENAMES (level-66) alias information
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ResolvedRenames {
152    /// Byte offset of the aliased range
153    pub offset: u32,
154    /// Total byte length of the aliased range
155    pub length: u32,
156    /// Paths of fields covered by this alias (in document order)
157    pub members: Vec<String>,
158}
159
160/// SIGN SEPARATE clause information
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct SignSeparateInfo {
163    /// Placement of the sign (LEADING or TRAILING)
164    pub placement: SignPlacement,
165}
166
167/// Sign placement for SIGN SEPARATE clause
168///
169/// # Examples
170///
171/// ```
172/// use copybook_core::SignPlacement;
173///
174/// let placement = SignPlacement::Leading;
175/// assert_eq!(placement, SignPlacement::Leading);
176/// assert_ne!(placement, SignPlacement::Trailing);
177/// ```
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180pub enum SignPlacement {
181    /// Sign byte precedes the numeric digits
182    Leading,
183    /// Sign byte follows the numeric digits
184    Trailing,
185}
186
187/// Array occurrence information
188///
189/// # Examples
190///
191/// ```
192/// use copybook_core::Occurs;
193///
194/// let fixed = Occurs::Fixed { count: 10 };
195/// assert!(matches!(fixed, Occurs::Fixed { count: 10 }));
196///
197/// let odo = Occurs::ODO {
198///     min: 0,
199///     max: 100,
200///     counter_path: "ITEM-COUNT".to_string(),
201/// };
202/// assert!(matches!(odo, Occurs::ODO { max: 100, .. }));
203/// ```
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub enum Occurs {
206    /// Fixed-size array
207    Fixed {
208        /// Number of elements
209        count: u32,
210    },
211    /// Variable-size array (OCCURS DEPENDING ON)
212    ODO {
213        /// Minimum number of elements
214        min: u32,
215        /// Maximum number of elements
216        max: u32,
217        /// Path to counter field
218        counter_path: String,
219    },
220}
221
222impl Schema {
223    /// Create a new empty schema
224    #[must_use]
225    pub fn new() -> Self {
226        Self {
227            fields: Vec::new(),
228            lrecl_fixed: None,
229            tail_odo: None,
230            fingerprint: String::new(),
231        }
232    }
233
234    /// Create a schema from a list of fields
235    #[must_use]
236    pub fn from_fields(fields: Vec<Field>) -> Self {
237        let mut schema = Self {
238            fields,
239            lrecl_fixed: None,
240            tail_odo: None,
241            fingerprint: String::new(),
242        };
243        schema.calculate_fingerprint();
244        schema
245    }
246
247    /// Calculate the schema fingerprint using SHA-256
248    pub fn calculate_fingerprint(&mut self) {
249        use sha2::{Digest, Sha256};
250
251        // Create canonical JSON representation
252        let canonical_json = self.create_canonical_json();
253
254        // Calculate SHA-256 hash
255        let mut hasher = Sha256::new();
256        hasher.update(canonical_json.as_bytes());
257
258        let result = hasher.finalize();
259        self.fingerprint = format!("{result:x}");
260    }
261
262    /// Create canonical JSON representation for fingerprinting
263    pub fn create_canonical_json(&self) -> String {
264        use serde_json::{Map, Value};
265
266        let mut schema_obj = Map::new();
267
268        // Add fields in canonical order
269        let fields_json: Vec<Value> = self
270            .fields
271            .iter()
272            .map(Self::field_to_canonical_json)
273            .collect();
274        schema_obj.insert("fields".to_string(), Value::Array(fields_json));
275
276        // Add schema-level properties
277        if let Some(lrecl) = self.lrecl_fixed {
278            schema_obj.insert("lrecl_fixed".to_string(), Value::Number(lrecl.into()));
279        }
280
281        if let Some(ref tail_odo) = self.tail_odo {
282            let mut tail_odo_obj = Map::new();
283            tail_odo_obj.insert(
284                "counter_path".to_string(),
285                Value::String(tail_odo.counter_path.clone()),
286            );
287            tail_odo_obj.insert(
288                "min_count".to_string(),
289                Value::Number(tail_odo.min_count.into()),
290            );
291            tail_odo_obj.insert(
292                "max_count".to_string(),
293                Value::Number(tail_odo.max_count.into()),
294            );
295            tail_odo_obj.insert(
296                "array_path".to_string(),
297                Value::String(tail_odo.array_path.clone()),
298            );
299            schema_obj.insert("tail_odo".to_string(), Value::Object(tail_odo_obj));
300        }
301
302        // Convert to canonical JSON string
303        serde_json::to_string(&Value::Object(schema_obj)).unwrap_or_default()
304    }
305
306    /// Convert field to canonical JSON for fingerprinting
307    fn field_to_canonical_json(field: &Field) -> Value {
308        use serde_json::{Map, Value};
309
310        let mut field_obj = Map::new();
311
312        // Add fields in canonical order
313        field_obj.insert("path".to_string(), Value::String(field.path.clone()));
314        field_obj.insert("name".to_string(), Value::String(field.name.clone()));
315        field_obj.insert("level".to_string(), Value::Number(field.level.into()));
316
317        // Add field kind
318        let kind_str = match &field.kind {
319            FieldKind::Alphanum { len } => format!("Alphanum({len})"),
320            FieldKind::ZonedDecimal {
321                digits,
322                scale,
323                signed,
324                sign_separate,
325            } => {
326                format!("ZonedDecimal({digits},{scale},{signed},{sign_separate:?})")
327            }
328            FieldKind::BinaryInt { bits, signed } => {
329                format!("BinaryInt({bits},{signed})")
330            }
331            FieldKind::PackedDecimal {
332                digits,
333                scale,
334                signed,
335            } => {
336                format!("PackedDecimal({digits},{scale},{signed})")
337            }
338            FieldKind::Group => "Group".to_string(),
339            FieldKind::Condition { values } => format!("Condition({values:?})"),
340            FieldKind::Renames {
341                from_field,
342                thru_field,
343            } => format!("Renames({from_field},{thru_field})"),
344            FieldKind::EditedNumeric {
345                pic_string,
346                width,
347                scale,
348                signed,
349            } => {
350                format!("EditedNumeric({pic_string},{width},scale={scale},signed={signed})")
351            }
352            FieldKind::FloatSingle => "FloatSingle".to_string(),
353            FieldKind::FloatDouble => "FloatDouble".to_string(),
354        };
355        field_obj.insert("kind".to_string(), Value::String(kind_str));
356
357        // Add optional fields
358        if let Some(ref redefines) = field.redefines_of {
359            field_obj.insert("redefines_of".to_string(), Value::String(redefines.clone()));
360        }
361
362        if let Some(ref occurs) = field.occurs {
363            let occurs_str = match occurs {
364                Occurs::Fixed { count } => format!("Fixed({count})"),
365                Occurs::ODO {
366                    min,
367                    max,
368                    counter_path,
369                } => {
370                    format!("ODO({min},{max},{counter_path})")
371                }
372            };
373            field_obj.insert("occurs".to_string(), Value::String(occurs_str));
374        }
375
376        if field.synchronized {
377            field_obj.insert("synchronized".to_string(), Value::Bool(true));
378        }
379
380        if field.blank_when_zero {
381            field_obj.insert("blank_when_zero".to_string(), Value::Bool(true));
382        }
383
384        // Add children recursively
385        if !field.children.is_empty() {
386            let children_json: Vec<Value> = field
387                .children
388                .iter()
389                .map(Self::field_to_canonical_json)
390                .collect();
391            field_obj.insert("children".to_string(), Value::Array(children_json));
392        }
393
394        Value::Object(field_obj)
395    }
396
397    /// Find a field by path
398    ///
399    /// Looks up a field by its fully-qualified dotted path (e.g., `"REC.ID"`).
400    /// Searches recursively through all nested groups.
401    ///
402    /// # Examples
403    ///
404    /// ```
405    /// use copybook_core::parse_copybook;
406    ///
407    /// let schema = parse_copybook("01 REC.\n   05 ID PIC 9(5).\n   05 NAME PIC X(20).").unwrap();
408    ///
409    /// let field = schema.find_field("REC.ID").unwrap();
410    /// assert_eq!(field.name, "ID");
411    /// assert_eq!(field.len, 5);
412    ///
413    /// assert!(schema.find_field("NONEXISTENT").is_none());
414    /// ```
415    #[must_use]
416    pub fn find_field(&self, path: &str) -> Option<&Field> {
417        Self::find_field_recursive(&self.fields, path)
418    }
419
420    fn find_field_recursive<'a>(fields: &'a [Field], path: &str) -> Option<&'a Field> {
421        for field in fields {
422            if field.path == path {
423                return Some(field);
424            }
425            if let Some(found) = Self::find_field_recursive(&field.children, path) {
426                return Some(found);
427            }
428        }
429        None
430    }
431
432    /// Find a field by path or RENAMES alias name
433    ///
434    /// This method first tries to find a field by its path using standard lookup.
435    /// If not found, it searches for a level-66 RENAMES field whose name matches
436    /// the query and returns that alias field.
437    ///
438    /// # Examples
439    ///
440    /// ```no_run
441    /// # use copybook_core::Schema;
442    /// let schema: Schema = // ... parsed schema with RENAMES
443    /// # Schema::new();
444    ///
445    /// // Direct field lookup
446    /// if let Some(field) = schema.find_field_or_alias("CUSTOMER-INFO") {
447    ///     println!("Found field: {}", field.name);
448    /// }
449    ///
450    /// // Alias lookup - finds level-66 field
451    /// if let Some(alias) = schema.find_field_or_alias("CUSTOMER-DETAILS") {
452    ///     if alias.level == 66 {
453    ///         println!("Found RENAMES alias: {}", alias.name);
454    ///     }
455    /// }
456    /// ```
457    #[must_use]
458    pub fn find_field_or_alias(&self, name_or_path: &str) -> Option<&Field> {
459        // First try direct field lookup
460        if let Some(field) = self.find_field(name_or_path) {
461            return Some(field);
462        }
463
464        // If not found, check if it's a RENAMES alias (level-66)
465        // We need to match by field name (last path component), not full path
466        let query_name = name_or_path.rsplit('.').next().unwrap_or(name_or_path);
467        Self::find_alias_field_recursive(&self.fields, query_name)
468    }
469
470    /// Recursively search for a level-66 RENAMES field by name
471    fn find_alias_field_recursive<'a>(fields: &'a [Field], alias_name: &str) -> Option<&'a Field> {
472        for field in fields {
473            // Check if this is a level-66 field with matching name
474            if field.level == 66 && field.name.eq_ignore_ascii_case(alias_name) {
475                return Some(field);
476            }
477            // Recurse into children
478            if let Some(found) = Self::find_alias_field_recursive(&field.children, alias_name) {
479                return Some(found);
480            }
481        }
482        None
483    }
484
485    /// Resolve a RENAMES alias to its first target field
486    ///
487    /// If the query matches a level-66 RENAMES alias, this method returns the
488    /// first storage-bearing field covered by that alias (from resolved_renames.members).
489    /// Otherwise, it performs standard field lookup.
490    ///
491    /// This is useful for codec integration where you want to decode/encode data
492    /// using an alias name but need the actual storage field.
493    ///
494    /// # Examples
495    ///
496    /// ```no_run
497    /// # use copybook_core::Schema;
498    /// let schema: Schema = // ... parsed schema with RENAMES
499    /// # Schema::new();
500    ///
501    /// // Resolve alias to target field
502    /// if let Some(target) = schema.resolve_alias_to_target("CUSTOMER-DETAILS") {
503    ///     // target will be CUSTOMER-INFO (or its first member)
504    ///     println!("Alias resolves to: {}", target.name);
505    /// }
506    /// ```
507    #[must_use]
508    pub fn resolve_alias_to_target(&self, name_or_path: &str) -> Option<&Field> {
509        // First try to find it as an alias
510        if let Some(alias_field) = self.find_field_or_alias(name_or_path) {
511            // If it's a level-66 with resolved_renames, return the first member
512            if alias_field.level == 66
513                && let Some(ref resolved) = alias_field.resolved_renames
514                && let Some(first_member_path) = resolved.members.first()
515            {
516                return self.find_field(first_member_path);
517            }
518            // Otherwise return the field itself
519            return Some(alias_field);
520        }
521        None
522    }
523
524    /// Find all fields that redefine the field at the given path
525    ///
526    /// Returns a list of fields whose `redefines_of` points to `target_path`.
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// use copybook_core::parse_copybook;
532    ///
533    /// let schema = parse_copybook(
534    ///     "01 REC.\n   05 AMT-NUM PIC 9(5)V99.\n   05 AMT-TXT REDEFINES AMT-NUM PIC X(7)."
535    /// ).unwrap();
536    ///
537    /// let redefs = schema.find_redefining_fields("AMT-NUM");
538    /// assert_eq!(redefs.len(), 1);
539    /// assert_eq!(redefs[0].name, "AMT-TXT");
540    /// ```
541    #[must_use]
542    pub fn find_redefining_fields<'a>(&'a self, target_path: &str) -> Vec<&'a Field> {
543        fn collect<'a>(fields: &'a [Field], target_path: &str, acc: &mut Vec<&'a Field>) {
544            for f in fields {
545                if let Some(ref redef) = f.redefines_of
546                    && redef == target_path
547                {
548                    acc.push(f);
549                }
550                collect(&f.children, target_path, acc);
551            }
552        }
553
554        let mut result = Vec::new();
555        collect(&self.fields, target_path, &mut result);
556        result
557    }
558
559    /// Get all fields in a flat list (pre-order traversal)
560    ///
561    /// Returns every field in the schema, including nested children,
562    /// as a flat vector in pre-order (depth-first) traversal order.
563    ///
564    /// # Examples
565    ///
566    /// ```
567    /// use copybook_core::parse_copybook;
568    ///
569    /// let schema = parse_copybook("01 REC.\n   05 ID PIC 9(5).\n   05 NAME PIC X(20).").unwrap();
570    ///
571    /// let all = schema.all_fields();
572    /// assert_eq!(all.len(), 3); // REC group + 2 leaf fields
573    /// assert_eq!(all[0].name, "REC");
574    /// assert_eq!(all[1].name, "ID");
575    /// assert_eq!(all[2].name, "NAME");
576    /// ```
577    #[must_use]
578    pub fn all_fields(&self) -> Vec<&Field> {
579        let mut result = Vec::new();
580        Self::collect_fields_recursive(&self.fields, &mut result);
581        result
582    }
583
584    fn collect_fields_recursive<'a>(fields: &'a [Field], result: &mut Vec<&'a Field>) {
585        for field in fields {
586            result.push(field);
587            Self::collect_fields_recursive(&field.children, result);
588        }
589    }
590}
591
592impl Field {
593    /// Create a new field with level and name
594    #[must_use]
595    pub fn new(level: u8, name: String) -> Self {
596        Self {
597            path: name.clone(),
598            name,
599            level,
600            kind: FieldKind::Group, // Default to group, will be updated by parser
601            offset: 0,
602            len: 0,
603            redefines_of: None,
604            occurs: None,
605            sync_padding: None,
606            synchronized: false,
607            blank_when_zero: false,
608            resolved_renames: None,
609            children: Vec::new(),
610        }
611    }
612
613    /// Create a new field with all parameters
614    #[must_use]
615    pub fn with_kind(level: u8, name: String, kind: FieldKind) -> Self {
616        Self {
617            path: name.clone(),
618            name,
619            level,
620            kind,
621            offset: 0,
622            len: 0,
623            redefines_of: None,
624            occurs: None,
625            sync_padding: None,
626            synchronized: false,
627            blank_when_zero: false,
628            resolved_renames: None,
629            children: Vec::new(),
630        }
631    }
632
633    /// Check if this is a group field
634    #[must_use]
635    pub fn is_group(&self) -> bool {
636        matches!(self.kind, FieldKind::Group)
637    }
638
639    /// Check if this is a scalar (leaf) field
640    #[must_use]
641    pub fn is_scalar(&self) -> bool {
642        !self.is_group()
643    }
644
645    /// Get the effective length including any arrays
646    #[must_use]
647    pub fn effective_length(&self) -> u32 {
648        match &self.occurs {
649            Some(Occurs::Fixed { count }) => self.len * count,
650            Some(Occurs::ODO { max, .. }) => self.len * max,
651            None => self.len,
652        }
653    }
654
655    /// Returns SIGN SEPARATE info if this is a zoned decimal field with that clause.
656    #[must_use]
657    pub fn sign_separate(&self) -> Option<&SignSeparateInfo> {
658        if let FieldKind::ZonedDecimal { sign_separate, .. } = &self.kind {
659            sign_separate.as_ref()
660        } else {
661            None
662        }
663    }
664
665    /// Returns true if this is a packed decimal (COMP-3) field.
666    #[must_use]
667    pub fn is_packed(&self) -> bool {
668        matches!(self.kind, FieldKind::PackedDecimal { .. })
669    }
670
671    /// Returns true if this is a binary integer (COMP/BINARY) field.
672    #[must_use]
673    pub fn is_binary(&self) -> bool {
674        matches!(self.kind, FieldKind::BinaryInt { .. })
675    }
676
677    /// Returns true if this field's name is FILLER (case-insensitive).
678    #[must_use]
679    pub fn is_filler(&self) -> bool {
680        self.name.eq_ignore_ascii_case("FILLER")
681    }
682}
683
684impl Default for Schema {
685    fn default() -> Self {
686        Self::new()
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693
694    #[test]
695    fn test_schema_default() {
696        let schema = Schema::default();
697        assert!(schema.fields.is_empty());
698        assert!(schema.lrecl_fixed.is_none());
699        assert!(schema.tail_odo.is_none());
700        // Fingerprint is empty for default schema (calculated later)
701        assert!(schema.fingerprint.is_empty());
702    }
703
704    #[test]
705    fn test_schema_new() {
706        let schema = Schema::new();
707        assert!(schema.fields.is_empty());
708        assert!(schema.lrecl_fixed.is_none());
709        assert!(schema.tail_odo.is_none());
710    }
711
712    #[test]
713    fn test_tail_odo_serialization() {
714        let tail_odo = TailODO {
715            counter_path: "ROOT.COUNT".to_string(),
716            min_count: 1,
717            max_count: 100,
718            array_path: "ROOT.ARRAY".to_string(),
719        };
720
721        let serialized = serde_json::to_string(&tail_odo).unwrap();
722        let deserialized: TailODO = serde_json::from_str(&serialized).unwrap();
723
724        assert_eq!(deserialized.counter_path, "ROOT.COUNT");
725        assert_eq!(deserialized.min_count, 1);
726        assert_eq!(deserialized.max_count, 100);
727        assert_eq!(deserialized.array_path, "ROOT.ARRAY");
728    }
729
730    #[test]
731    fn test_field_new() {
732        let field = Field::new(5, "TEST-FIELD".to_string());
733        assert_eq!(field.level, 5);
734        assert_eq!(field.name, "TEST-FIELD");
735        assert_eq!(field.path, "TEST-FIELD");
736        assert!(matches!(field.kind, FieldKind::Group));
737        assert_eq!(field.offset, 0);
738        assert_eq!(field.len, 0);
739    }
740
741    #[test]
742    fn test_field_with_kind() {
743        let kind = FieldKind::Alphanum { len: 10 };
744        let field = Field::with_kind(5, "TEST-FIELD".to_string(), kind.clone());
745        assert_eq!(field.level, 5);
746        assert_eq!(field.name, "TEST-FIELD");
747        assert!(matches!(field.kind, FieldKind::Alphanum { len: 10 }));
748    }
749
750    #[test]
751    fn test_field_is_group() {
752        let group_field = Field::new(1, "GROUP".to_string());
753        assert!(group_field.is_group());
754        assert!(!group_field.is_scalar());
755
756        let scalar_field =
757            Field::with_kind(5, "SCALAR".to_string(), FieldKind::Alphanum { len: 10 });
758        assert!(!scalar_field.is_group());
759        assert!(scalar_field.is_scalar());
760    }
761
762    #[test]
763    fn test_field_effective_length_no_occurs() {
764        let field = Field {
765            path: "TEST".to_string(),
766            name: "TEST".to_string(),
767            level: 5,
768            kind: FieldKind::Alphanum { len: 10 },
769            offset: 0,
770            len: 10,
771            redefines_of: None,
772            occurs: None,
773            sync_padding: None,
774            synchronized: false,
775            blank_when_zero: false,
776            resolved_renames: None,
777            children: Vec::new(),
778        };
779        assert_eq!(field.effective_length(), 10);
780    }
781
782    #[test]
783    fn test_field_effective_length_fixed_occurs() {
784        let field = Field {
785            path: "TEST".to_string(),
786            name: "TEST".to_string(),
787            level: 5,
788            kind: FieldKind::Alphanum { len: 10 },
789            offset: 0,
790            len: 10,
791            redefines_of: None,
792            occurs: Some(Occurs::Fixed { count: 5 }),
793            sync_padding: None,
794            synchronized: false,
795            blank_when_zero: false,
796            resolved_renames: None,
797            children: Vec::new(),
798        };
799        assert_eq!(field.effective_length(), 50);
800    }
801
802    #[test]
803    fn test_field_effective_length_odo_occurs() {
804        let field = Field {
805            path: "TEST".to_string(),
806            name: "TEST".to_string(),
807            level: 5,
808            kind: FieldKind::Alphanum { len: 10 },
809            offset: 0,
810            len: 10,
811            redefines_of: None,
812            occurs: Some(Occurs::ODO {
813                min: 1,
814                max: 100,
815                counter_path: "ROOT.COUNT".to_string(),
816            }),
817            sync_padding: None,
818            synchronized: false,
819            blank_when_zero: false,
820            resolved_renames: None,
821            children: Vec::new(),
822        };
823        assert_eq!(field.effective_length(), 1000);
824    }
825
826    #[test]
827    fn test_field_kind_serialization() {
828        let kinds = vec![
829            FieldKind::Alphanum { len: 10 },
830            FieldKind::ZonedDecimal {
831                digits: 5,
832                scale: 2,
833                signed: true,
834                sign_separate: None,
835            },
836            FieldKind::BinaryInt {
837                bits: 32,
838                signed: true,
839            },
840            FieldKind::PackedDecimal {
841                digits: 7,
842                scale: 2,
843                signed: true,
844            },
845            FieldKind::Group,
846            FieldKind::Condition {
847                values: vec!["A".to_string(), "B".to_string()],
848            },
849            FieldKind::Renames {
850                from_field: "FIELD1".to_string(),
851                thru_field: "FIELD2".to_string(),
852            },
853            FieldKind::EditedNumeric {
854                pic_string: "ZZ9.99".to_string(),
855                width: 6,
856                scale: 2,
857                signed: false,
858            },
859        ];
860
861        for kind in kinds {
862            let serialized = serde_json::to_string(&kind).unwrap();
863            let deserialized: FieldKind = serde_json::from_str(&serialized).unwrap();
864            // Can't directly compare FieldKind due to no PartialEq, so re-serialize and compare
865            let re_serialized = serde_json::to_string(&deserialized).unwrap();
866            assert_eq!(serialized, re_serialized);
867        }
868    }
869
870    #[test]
871    fn test_sign_placement_serialization() {
872        let placements = vec![SignPlacement::Leading, SignPlacement::Trailing];
873
874        for placement in placements {
875            let serialized = serde_json::to_string(&placement).unwrap();
876            let deserialized: SignPlacement = serde_json::from_str(&serialized).unwrap();
877            assert_eq!(serialized, serde_json::to_string(&deserialized).unwrap());
878        }
879    }
880
881    #[test]
882    fn test_sign_separate_info_serialization() {
883        let info = SignSeparateInfo {
884            placement: SignPlacement::Leading,
885        };
886
887        let serialized = serde_json::to_string(&info).unwrap();
888        let deserialized: SignSeparateInfo = serde_json::from_str(&serialized).unwrap();
889
890        assert!(matches!(deserialized.placement, SignPlacement::Leading));
891    }
892
893    #[test]
894    fn test_resolved_renames_serialization() {
895        let renames = ResolvedRenames {
896            offset: 10,
897            length: 50,
898            members: vec!["FIELD1".to_string(), "FIELD2".to_string()],
899        };
900
901        let serialized = serde_json::to_string(&renames).unwrap();
902        let deserialized: ResolvedRenames = serde_json::from_str(&serialized).unwrap();
903
904        assert_eq!(deserialized.offset, 10);
905        assert_eq!(deserialized.length, 50);
906        assert_eq!(deserialized.members.len(), 2);
907        assert_eq!(deserialized.members[0], "FIELD1");
908        assert_eq!(deserialized.members[1], "FIELD2");
909    }
910
911    #[test]
912    fn test_schema_serialization() {
913        let schema = Schema {
914            fields: vec![Field::new(1, "ROOT".to_string())],
915            lrecl_fixed: Some(100),
916            tail_odo: Some(TailODO {
917                counter_path: "ROOT.COUNT".to_string(),
918                min_count: 1,
919                max_count: 100,
920                array_path: "ROOT.ARRAY".to_string(),
921            }),
922            fingerprint: "test-fingerprint".to_string(),
923        };
924
925        let serialized = serde_json::to_string(&schema).unwrap();
926        let deserialized: Schema = serde_json::from_str(&serialized).unwrap();
927
928        assert_eq!(deserialized.fields.len(), 1);
929        assert_eq!(deserialized.lrecl_fixed, Some(100));
930        assert!(deserialized.tail_odo.is_some());
931        assert_eq!(deserialized.fingerprint, "test-fingerprint");
932    }
933
934    #[test]
935    fn test_schema_find_field() {
936        let mut field = Field::new(5, "CHILD".to_string());
937        field.path = "PARENT.CHILD".to_string();
938
939        let schema = Schema {
940            fields: vec![Field {
941                path: "PARENT".to_string(),
942                name: "PARENT".to_string(),
943                level: 1,
944                kind: FieldKind::Group,
945                offset: 0,
946                len: 10,
947                redefines_of: None,
948                occurs: None,
949                sync_padding: None,
950                synchronized: false,
951                blank_when_zero: false,
952                resolved_renames: None,
953                children: vec![field],
954            }],
955            lrecl_fixed: None,
956            tail_odo: None,
957            fingerprint: "test".to_string(),
958        };
959
960        let found = schema.find_field("PARENT.CHILD");
961        assert!(found.is_some());
962        assert_eq!(found.unwrap().name, "CHILD");
963
964        let not_found = schema.find_field("NONEXISTENT");
965        assert!(not_found.is_none());
966    }
967
968    #[test]
969    fn test_schema_find_redefining_fields() {
970        let _base_field = Field::new(5, "BASE".to_string());
971        let redef_field1 =
972            Field::with_kind(5, "REDEF1".to_string(), FieldKind::Alphanum { len: 5 });
973        let redef_field2 = Field::with_kind(
974            5,
975            "REDEF2".to_string(),
976            FieldKind::ZonedDecimal {
977                digits: 5,
978                scale: 0,
979                signed: false,
980                sign_separate: None,
981            },
982        );
983
984        let mut base_field_with_redef = Field::new(5, "BASE".to_string());
985        base_field_with_redef.path = "ROOT.BASE".to_string();
986        base_field_with_redef.redefines_of = None;
987
988        let mut redef_field1_with_path = redef_field1.clone();
989        redef_field1_with_path.path = "ROOT.REDEF1".to_string();
990        redef_field1_with_path.redefines_of = Some("ROOT.BASE".to_string());
991
992        let mut redef_field2_with_path = redef_field2.clone();
993        redef_field2_with_path.path = "ROOT.REDEF2".to_string();
994        redef_field2_with_path.redefines_of = Some("ROOT.BASE".to_string());
995
996        let schema = Schema {
997            fields: vec![
998                base_field_with_redef,
999                redef_field1_with_path,
1000                redef_field2_with_path,
1001            ],
1002            lrecl_fixed: None,
1003            tail_odo: None,
1004            fingerprint: "test".to_string(),
1005        };
1006
1007        let redefining = schema.find_redefining_fields("ROOT.BASE");
1008        assert_eq!(redefining.len(), 2);
1009        assert!(redefining.iter().any(|f| f.name == "REDEF1"));
1010        assert!(redefining.iter().any(|f| f.name == "REDEF2"));
1011    }
1012
1013    #[test]
1014    fn test_schema_all_fields() {
1015        let child1 = Field::with_kind(5, "CHILD1".to_string(), FieldKind::Alphanum { len: 5 });
1016        let child2 = Field::with_kind(5, "CHILD2".to_string(), FieldKind::Alphanum { len: 5 });
1017
1018        let mut parent = Field::new(1, "PARENT".to_string());
1019        parent.path = "ROOT.PARENT".to_string();
1020        parent.children = vec![child1, child2];
1021
1022        let top_level = Field::with_kind(1, "TOP".to_string(), FieldKind::Alphanum { len: 10 });
1023
1024        let schema = Schema {
1025            fields: vec![parent, top_level],
1026            lrecl_fixed: None,
1027            tail_odo: None,
1028            fingerprint: "test".to_string(),
1029        };
1030
1031        let all_fields = schema.all_fields();
1032        // Should have: parent, child1, child2, top_level
1033        assert_eq!(all_fields.len(), 4);
1034        assert!(all_fields.iter().any(|f| f.name == "PARENT"));
1035        assert!(all_fields.iter().any(|f| f.name == "CHILD1"));
1036        assert!(all_fields.iter().any(|f| f.name == "CHILD2"));
1037        assert!(all_fields.iter().any(|f| f.name == "TOP"));
1038    }
1039}