|
| 1 | +# TinyAML - Ada YAML Library |
| 2 | + |
| 3 | +A strict YAML subset parser for Ada 2022, inspired by [StrictYAML](https://github.com/crdoconnor/strictyaml). Designed for configuration files with a hybrid validation approach: parse to untyped tree, optionally validate with schemas. |
| 4 | + |
| 5 | +## Build Commands |
| 6 | + |
| 7 | +All build commands must run in a distrobox container: |
| 8 | + |
| 9 | +```bash |
| 10 | +distrobox enter ubuntu -- alr build # Build library |
| 11 | +distrobox enter ubuntu -- bash -c 'cd tests && alr build' # Build tests |
| 12 | +distrobox enter ubuntu -- bash -c 'cd examples && alr build' # Build examples |
| 13 | +distrobox enter ubuntu -- ./tests/bin/test_runner # Run tests |
| 14 | +``` |
| 15 | + |
| 16 | +## Project Structure |
| 17 | + |
| 18 | +``` |
| 19 | +tinyaml_ada/ |
| 20 | +├── alire.toml # Alire package manifest (crate: tinyaml) |
| 21 | +├── tinyaml.gpr # Main GPR project file |
| 22 | +├── src/ |
| 23 | +│ ├── tinyaml.ads/adb # Root: exceptions, Source_Position, Source_Span |
| 24 | +│ ├── tinyaml-lexer.ads/adb # Tokenizer with indentation tracking |
| 25 | +│ ├── tinyaml-nodes.ads/adb # Abstract YAML_Node, Node_Access, type checking |
| 26 | +│ ├── tinyaml-nodes-scalar.ads/adb # Scalar_Node |
| 27 | +│ ├── tinyaml-nodes-sequence.ads/adb # Sequence_Node |
| 28 | +│ ├── tinyaml-nodes-map.ads/adb # Map_Node |
| 29 | +│ ├── tinyaml-nodes-navigation.ads/adb # Navigate, Get_String |
| 30 | +│ ├── tinyaml-nodes-prelude.ads # Convenience re-exports |
| 31 | +│ ├── tinyaml-parser.ads/adb # Recursive descent parser |
| 32 | +│ ├── tinyaml-schemas.ads/adb # Abstract Schema base |
| 33 | +│ ├── tinyaml-schemas-*.ads/adb # Concrete schema types |
| 34 | +│ ├── tinyaml-schemas-prelude.ads # Convenience re-exports |
| 35 | +│ └── tinyaml-validation.ads/adb # Validation engine |
| 36 | +├── tests/ |
| 37 | +│ ├── tinyaml_tests.gpr |
| 38 | +│ ├── test_harness.ads/adb # Simple test framework |
| 39 | +│ ├── test_lexer.ads/adb |
| 40 | +│ ├── test_parser.ads/adb |
| 41 | +│ ├── test_validation.ads/adb |
| 42 | +│ └── test_runner.adb |
| 43 | +└── examples/ |
| 44 | + ├── tinyaml_examples.gpr |
| 45 | + ├── basic_parsing.adb # Parse without validation |
| 46 | + ├── with_validation.adb # Schema validation |
| 47 | + └── error_handling.adb # Error handling patterns |
| 48 | +``` |
| 49 | + |
| 50 | +## Design Decisions |
| 51 | + |
| 52 | +- **Ada 2022** with FSF GNAT compatible subset (no user-defined literals, parallel blocks) |
| 53 | +- **StrictYAML philosophy**: No implicit typing, no flow style, no anchors/aliases, no tags |
| 54 | +- **Hybrid validation**: Parse to untyped tree, optional schema validation layer |
| 55 | +- **OOP design**: Tagged type hierarchies for nodes and schemas |
| 56 | +- **Strings only**: No file I/O - application provides content as String |
| 57 | + |
| 58 | +## API Style |
| 59 | + |
| 60 | +Uses idiomatic Ada with named access types and function call notation. |
| 61 | +Prelude packages provide convenient imports: |
| 62 | + |
| 63 | +```ada |
| 64 | +with Tinyaml.Parser; |
| 65 | +with Tinyaml.Nodes; use Tinyaml.Nodes; |
| 66 | +with Tinyaml.Nodes.Prelude; use Tinyaml.Nodes.Prelude; |
| 67 | +with Tinyaml.Schemas.Prelude; use Tinyaml.Schemas.Prelude; |
| 68 | +
|
| 69 | +Doc := Tinyaml.Parser.Parse (Config); |
| 70 | +
|
| 71 | +-- Navigation with Node_Access |
| 72 | +Put_Line (Get_String (Doc, "database.host")); |
| 73 | +Features := Navigate (Doc, "features"); |
| 74 | +
|
| 75 | +-- Type checking requires .all |
| 76 | +if Is_Sequence (Features.all) then ... |
| 77 | +
|
| 78 | +-- Get scalar value from Node_Access |
| 79 | +Put_Line (Value (Seq.Element (I))); |
| 80 | +``` |
| 81 | + |
| 82 | +## YAML Features |
| 83 | + |
| 84 | +**Supported:** |
| 85 | +- Block scalars (literal `|`, folded `>`) |
| 86 | +- Block sequences (`-`) |
| 87 | +- Block mappings (`:`) |
| 88 | +- Comments (`#`) |
| 89 | +- Quoted strings (single and double) |
| 90 | +- Escape sequences in double-quoted strings |
| 91 | + |
| 92 | +**Rejected (raises Parse_Error):** |
| 93 | +- Flow style (`{...}`, `[...]`) |
| 94 | +- Anchors and aliases (`&`, `*`) |
| 95 | +- Tags (`!`) |
| 96 | +- Duplicate keys |
| 97 | + |
| 98 | +## Known Issues |
| 99 | + |
| 100 | +- Style warnings for alphabetical ordering - intentionally deferred |
| 101 | +- Some internal helper functions lack specs - intentionally local |
| 102 | + |
| 103 | +## Schema API (Redesigned) |
| 104 | + |
| 105 | +The schema API uses class-wide types with dot notation: |
| 106 | + |
| 107 | +1. **Direct instantiation with aggregates** - no factory functions where possible |
| 108 | +2. **Class-wide types** for dot-notation enabled interface |
| 109 | +3. **Hidden access types** - users work with tagged types directly |
| 110 | +4. **Procedural field addition** instead of functional chaining |
| 111 | +5. **Optional as a field modifier** instead of a wrapper type |
| 112 | +6. **Abstract Constraint hierarchy** for extensible validation rules |
| 113 | +7. **Seq_Item holder** for polymorphic sequence item storage |
| 114 | + |
| 115 | +### Type Hierarchies |
| 116 | + |
| 117 | +``` |
| 118 | +Schema (abstract) Constraint (abstract) |
| 119 | +├── Str_Schema └── Range_Constraint |
| 120 | +├── Int_Schema (future: Length_Constraint, |
| 121 | +├── Float_Schema Pattern_Constraint, etc.) |
| 122 | +├── Bool_Schema |
| 123 | +├── Enum_Schema |
| 124 | +├── Seq_Schema |
| 125 | +├── Map_Schema |
| 126 | +└── Any_Schema |
| 127 | +``` |
| 128 | + |
| 129 | +### API |
| 130 | + |
| 131 | +```ada |
| 132 | +package Tinyaml.Schemas is |
| 133 | +
|
| 134 | + -- Schema hierarchy (no access types exposed to users) |
| 135 | + type Schema is abstract tagged private; |
| 136 | +
|
| 137 | + function Is_Valid (S : Schema; N : Nodes.YAML_Node'Class) return Boolean is abstract; |
| 138 | + function Describe (S : Schema) return String is abstract; |
| 139 | +
|
| 140 | + -- Constraint hierarchy |
| 141 | + type Constraint is abstract tagged private; |
| 142 | +
|
| 143 | + function Check (C : Constraint; Value : String) return Boolean is abstract; |
| 144 | + function Describe (C : Constraint) return String is abstract; |
| 145 | +
|
| 146 | + -- Range_Constraint (first child of Constraint) |
| 147 | + type Range_Constraint is new Constraint with private; |
| 148 | +
|
| 149 | + --------------------------------------------------------------------------- |
| 150 | + -- Seq_Item Holder (for polymorphic sequence items) |
| 151 | + --------------------------------------------------------------------------- |
| 152 | +
|
| 153 | + type Seq_Item is tagged private; |
| 154 | +
|
| 155 | + function Seq_Item (S : Schema'Class) return Seq_Item; |
| 156 | +
|
| 157 | + --------------------------------------------------------------------------- |
| 158 | + -- Concrete Schema Types (instantiate directly with aggregates) |
| 159 | + --------------------------------------------------------------------------- |
| 160 | +
|
| 161 | + type Str_Schema is new Schema with private; |
| 162 | + type Float_Schema is new Schema with private; |
| 163 | + type Bool_Schema is new Schema with private; |
| 164 | + type Any_Schema is new Schema with private; |
| 165 | +
|
| 166 | + type Int_Schema is new Schema with private; |
| 167 | + -- Use aggregate: (Constraint => (Min => 1, Max => 100)) |
| 168 | +
|
| 169 | + type Enum_Schema is new Schema with private; |
| 170 | + -- Use aggregate: (Values => String_Vectors.To_Vector(...)) |
| 171 | +
|
| 172 | + type Seq_Schema is new Schema with private; |
| 173 | + -- Use aggregate: (Item => Seq_Item (Str_Schema'(...))) |
| 174 | +
|
| 175 | + type Map_Schema is new Schema with private; |
| 176 | + -- Instantiate directly, then add fields with dot notation |
| 177 | +
|
| 178 | + --------------------------------------------------------------------------- |
| 179 | + -- Field Addition (class-wide parameters enable dot notation) |
| 180 | + --------------------------------------------------------------------------- |
| 181 | +
|
| 182 | + procedure Str (M : in out Map_Schema'Class; Name : String; |
| 183 | + Optional : Boolean := False); |
| 184 | + procedure Int (M : in out Map_Schema'Class; Name : String; |
| 185 | + Optional : Boolean := False); |
| 186 | + procedure Int (M : in out Map_Schema'Class; Name : String; |
| 187 | + Constraint : Range_Constraint; |
| 188 | + Optional : Boolean := False); |
| 189 | + procedure Flt (M : in out Map_Schema'Class; Name : String; |
| 190 | + Optional : Boolean := False); |
| 191 | + procedure Bool (M : in out Map_Schema'Class; Name : String; |
| 192 | + Optional : Boolean := False; |
| 193 | + Default : String := ""); |
| 194 | + procedure Enum (M : in out Map_Schema'Class; Name : String; |
| 195 | + Values : String_Array; |
| 196 | + Optional : Boolean := False); |
| 197 | + procedure Seq (M : in out Map_Schema'Class; Name : String; |
| 198 | + Item : Seq_Item; |
| 199 | + Optional : Boolean := False); |
| 200 | + procedure Any (M : in out Map_Schema'Class; Name : String; |
| 201 | + Optional : Boolean := False); |
| 202 | +
|
| 203 | + -- Nested map (returns the nested map for further configuration) |
| 204 | + function Map (M : in out Map_Schema'Class; Name : String; |
| 205 | + Optional : Boolean := False) return Map_Schema; |
| 206 | +
|
| 207 | +private |
| 208 | +
|
| 209 | + type Schema is abstract tagged null record; |
| 210 | +
|
| 211 | + type Constraint is abstract tagged null record; |
| 212 | +
|
| 213 | + type Range_Constraint is new Constraint with record |
| 214 | + Min : Integer := Integer'First; |
| 215 | + Max : Integer := Integer'Last; |
| 216 | + end record; |
| 217 | +
|
| 218 | + -- Access types hidden from users, used internally for storage |
| 219 | + type Schema_Access is access all Schema'Class; |
| 220 | +
|
| 221 | + -- Seq_Item wraps access type for clean aggregate syntax |
| 222 | + type Seq_Item is tagged record |
| 223 | + Schema : Schema_Access; |
| 224 | + end record; |
| 225 | +
|
| 226 | + type Str_Schema is new Schema with null record; |
| 227 | + type Float_Schema is new Schema with null record; |
| 228 | + type Bool_Schema is new Schema with null record; |
| 229 | + type Any_Schema is new Schema with null record; |
| 230 | +
|
| 231 | + type Int_Schema is new Schema with record |
| 232 | + Constraint : Range_Constraint := (others => <>); |
| 233 | + end record; |
| 234 | +
|
| 235 | + type Enum_Schema is new Schema with record |
| 236 | + Values : String_Vectors.Vector; |
| 237 | + end record; |
| 238 | +
|
| 239 | + type Seq_Schema is new Schema with record |
| 240 | + Item : Seq_Item; |
| 241 | + end record; |
| 242 | +
|
| 243 | + type Field_Info is record |
| 244 | + Field_Schema : Schema_Access; |
| 245 | + Is_Required : Boolean := True; |
| 246 | + Default_Val : Ada.Strings.Unbounded.Unbounded_String; |
| 247 | + end record; |
| 248 | +
|
| 249 | + type Map_Schema is new Schema with record |
| 250 | + Fields : Field_Maps.Map; |
| 251 | + end record; |
| 252 | +
|
| 253 | +end Tinyaml.Schemas; |
| 254 | +``` |
| 255 | + |
| 256 | +### Usage Example |
| 257 | + |
| 258 | +```ada |
| 259 | +with Tinyaml.Schemas; use Tinyaml.Schemas; |
| 260 | +
|
| 261 | +procedure Define_Config_Schema is |
| 262 | + -- Direct instantiation, no factory functions |
| 263 | + M : Map_Schema; |
| 264 | +begin |
| 265 | + -- Simple fields with dot notation |
| 266 | + M.Str ("host"); |
| 267 | + M.Str ("name"); |
| 268 | + M.Int ("port", Constraint => (Min => 1, Max => 65535)); |
| 269 | + M.Bool ("debug", Optional => True, Default => "false"); |
| 270 | +
|
| 271 | + -- Nested map |
| 272 | + declare |
| 273 | + DB : Map_Schema := M.Map ("database"); |
| 274 | + begin |
| 275 | + DB.Str ("driver"); |
| 276 | + DB.Str ("connection"); |
| 277 | + end; |
| 278 | +
|
| 279 | + -- Sequence of strings |
| 280 | + M.Seq ("features", Item => Seq_Item (Str_Schema'(others => <>)), |
| 281 | + Optional => True); |
| 282 | +
|
| 283 | + -- Sequence of maps (e.g., Docker Compose volumes) |
| 284 | + declare |
| 285 | + Volume_Schema : Map_Schema; |
| 286 | + begin |
| 287 | + Volume_Schema.Str ("type"); |
| 288 | + Volume_Schema.Str ("source"); |
| 289 | + Volume_Schema.Str ("target"); |
| 290 | + M.Seq ("volumes", Item => Seq_Item (Volume_Schema)); |
| 291 | + end; |
| 292 | +end Define_Config_Schema; |
| 293 | +``` |
| 294 | + |
| 295 | +### Key Design Features |
| 296 | + |
| 297 | +- **Direct instantiation** - `M : Map_Schema;` |
| 298 | +- **Dot notation** - `M.Str ("host")` for building schemas |
| 299 | +- **Aggregates for construction** - `(Constraint => (Min => 1, Max => 100))` |
| 300 | +- **Seq_Item holder** - `To_Seq_Item (Map_Schema'(...))` for polymorphic sequences |
| 301 | +- **Class-wide interface** - users work with tagged types, not access types |
| 302 | +- **Hidden access types** - only in private part, for internal storage |
| 303 | +- **Optional as parameter** - `Optional => True` instead of wrapper type |
| 304 | +- **Abstract `Constraint` hierarchy** - extensible (start with `Range_Constraint`) |
0 commit comments