@homedev/objector 1.3.8 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,608 @@
1
+ # @homedev/objector
2
+
3
+ Object processing library for YAML/JSON with support for includes, field processors, conditional logic, and extensibility.
4
+
5
+ ## Overview
6
+
7
+ Objector is a comprehensive tool for loading, processing, and transforming YAML/JSON documents with advanced features like:
8
+
9
+ - 🔄 Document includes and composition
10
+ - 🔌 Extensible field processors and data sources
11
+ - 🎯 Conditional logic and flow control
12
+ - 📦 Module system with custom functions
13
+ - 🔍 Variable substitution and selectors
14
+ - 📝 Output generation and file writing
15
+ - 🎨 Template inheritance and extensions
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ bun add @homedev/objector
21
+ # or
22
+ npm install @homedev/objector
23
+ # or
24
+ pnpm add @homedev/objector
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```typescript
30
+ import { Objector } from '@homedev/objector'
31
+
32
+ const objector = new Objector()
33
+
34
+ // Load and process a YAML/JSON file
35
+ const result = await objector.load('config.yaml')
36
+ ```
37
+
38
+ ```typescript
39
+ import { Objector } from '@homedev/objector'
40
+
41
+ const objector = new Objector()
42
+ .variables({ env: 'production', version: '1.0.0' }) // Add custom variables
43
+ .include('settings.yaml') // Include additional files
44
+
45
+ // Process the document
46
+ const processed = await objector.load('main.yaml')
47
+ ```
48
+
49
+ ## Core Features
50
+
51
+ ### 1. Document Loading and Includes
52
+
53
+ Load YAML/JSON files with automatic format detection and includes:
54
+
55
+ ```typescript
56
+ const objector = new Objector()
57
+
58
+ // Set include directories
59
+ objector.includeDirectory('./config', './templates')
60
+
61
+ // Load a file (automatically resolves from include directories)
62
+ const config = await objector.load('app-config.yaml')
63
+ ```
64
+
65
+ **In your YAML/JSON:**
66
+ ```yaml
67
+ includes:
68
+ - base-config.yaml
69
+ - settings/*.yaml
70
+ - ../shared/defaults.yaml
71
+
72
+ myConfig:
73
+ setting: value
74
+ ```
75
+
76
+ ### 2. Variables and Substitution
77
+
78
+ Inject and reference variables throughout your documents:
79
+
80
+ ```typescript
81
+ objector.variables({
82
+ env: 'production',
83
+ apiUrl: 'https://api.example.com',
84
+ version: '2.1.0'
85
+ })
86
+ ```
87
+
88
+ **Usage in documents:**
89
+ ```yaml
90
+ app:
91
+ environment: $(env)
92
+ api: $(apiUrl)
93
+ version: $(version)
94
+ ```
95
+
96
+ ### 3. Conditional Logic
97
+
98
+ Control document structure with conditions:
99
+
100
+ ```yaml
101
+ features:
102
+ - name: cache
103
+ .if: $(eq:env, "production")
104
+ .then:
105
+ enabled: true
106
+ ttl: 3600
107
+ .else:
108
+ enabled: false
109
+
110
+ - name: debug
111
+ .skip: $(eq:env, "production")
112
+ logging: verbose
113
+ ```
114
+
115
+ ### 4. Loops and Iteration
116
+
117
+ Generate repeated structures with `each`:
118
+
119
+ ```yaml
120
+ environments:
121
+ .each:
122
+ .from:
123
+ - dev
124
+ - staging
125
+ - prod
126
+ .model:
127
+ name: $(item)
128
+ url: https://$(item).example.com
129
+ replicas: $(switch:
130
+ from: $(item)
131
+ cases:
132
+ prod: 5
133
+ staging: 2
134
+ default: 1)
135
+ ```
136
+
137
+ ### 5. Template Inheritance
138
+
139
+ Extend and merge templates:
140
+
141
+ ```yaml
142
+ baseTemplate:
143
+ server:
144
+ port: 8080
145
+ timeout: 30
146
+
147
+ production:
148
+ extends: .baseTemplate
149
+ server:
150
+ port: 443
151
+ ssl: true
152
+ # Merged result: port=443, timeout=30, ssl=true
153
+ ```
154
+
155
+ ### 6. File Operations
156
+
157
+ Read files and scan directories:
158
+
159
+ ```yaml
160
+ certificate: $(file:./certs/server.crt)
161
+
162
+ migrations:
163
+ .from: $(scan:"*.sql", "./migrations")
164
+ .model:
165
+ name: $(item)
166
+ content: $(file:$(item))
167
+ ```
168
+
169
+ ### 7. Custom Modules
170
+
171
+ Add JavaScript functions as data sources:
172
+
173
+ ```yaml
174
+ modules:
175
+ utils: |
176
+ export function formatDate(ctx, date) {
177
+ return new Date(date).toISOString()
178
+ }
179
+ export function generateId(ctx, prefix) {
180
+ return `${prefix}-${Math.random().toString(36).substr(2, 9)}`
181
+ }
182
+
183
+ records:
184
+ - id: $(utils.generateId:"rec")
185
+ created: $(utils.formatDate:"2024-01-01")
186
+ ```
187
+
188
+ ### 8. Output Generation
189
+
190
+ Generate multiple output files from one source:
191
+
192
+ ```yaml
193
+ output:
194
+ - file: dist/config.json
195
+ content:
196
+ version: $(version)
197
+ settings: $(config)
198
+
199
+ - file: dist/README.md
200
+ content: |
201
+ # Configuration
202
+ Version: $(version)
203
+ ```
204
+
205
+ ```typescript
206
+ // Write all outputs to disk
207
+ await objector.writeAll()
208
+ ```
209
+
210
+ ## Built-in Keys
211
+
212
+ Keys are object properties prefixed with `.` and are used for structure/flow control.
213
+
214
+ - `.output` - Register output payload(s)
215
+ - `.load` - Load one or more additional files
216
+ - `.each` - Repeat a model over a source list
217
+ - `.map` - Map a source list into a transformed list
218
+ - `.from` - Resolve a selector/reference into current node
219
+ - `.concat` - Concatenate arrays from selectors
220
+ - `.extends` - Merge from one or many templates
221
+ - `.group` - Expand grouped items into parent list
222
+ - `.if` - Conditional branch using `.then` / `.else`
223
+ - `.switch` - Case-based branching with `cases` / `default`
224
+ - `.skip` - Remove parent when condition is true
225
+ - `.metadata` - Attach metadata for a path
226
+ - `.modules` - Register source functions from inline JS modules
227
+ - `.try` - Guard expression with optional `.catch`
228
+ - `.let` - Introduce temporary variables into context
229
+ - `.tap` - Debug current path/value to console
230
+
231
+ ## Built-in Data Sources
232
+
233
+ Sources are used with `$()` syntax.
234
+
235
+ ### Core Sources
236
+ - Files: `include`, `exists`, `file`, `scan`
237
+ - String/format: `substring`, `repeat`, `pad`, `formatAs`
238
+ - List: `merge`, `join`, `range`, `filter`
239
+ - Conditions: `if`, `check`, `and`, `or`, `xor`, `true`, `false`, `eq`, `ne`, `lt`, `le`, `gt`, `ge`, `in`, `notin`, `contains`, `notcontains`, `containsi`, `notcontainsi`, `null`, `notnull`, `empty`, `notempty`, `nullorempty`, `notnullorempty`
240
+ - Types: `convert`, `isType`, `typeOf`
241
+ - Flow helpers: `each`, `switch`, `default`, `section`
242
+
243
+ ### Built-in Macro Sources
244
+ These are also available as `$()` sources by default.
245
+
246
+ - String/text: `env`, `uuid`, `pascal`, `camel`, `capital`, `snake`, `kebab`, `len`, `reverse`, `concat`, `trim`, `ltrim`, `rtrim`, `lower`, `upper`, `encode`, `decode`
247
+ - Collection/object: `list`, `object`, `unique`, `groupBy`, `chunk`, `flatten`, `compact`, `head`, `tail`
248
+ - Path/filepath: `pathJoin`, `resolve`, `dirname`, `basename`, `normalize`, `extname`, `relative`, `isAbsolute`, `segments`
249
+ - Hash/encoding: `md5`, `sha1`, `sha256`, `sha512`, `hash`, `hmac`, `base64Encode`, `base64Decode`, `hexEncode`, `hexDecode`
250
+ - Math: `add`, `subtract`, `multiply`, `divide`, `modulo`, `power`, `sqrt`, `abs`, `floor`, `ceil`, `round`, `min`, `max`, `clamp`, `random`
251
+ - Date/time: `timestamp`, `iso`, `utc`, `formatDate`, `parseDate`, `year`, `month`, `day`, `dayOfWeek`, `hours`, `minutes`, `seconds`
252
+ - Regex: `regexTest`, `regexMatch`, `regexMatchAll`, `regexReplace`, `regexReplaceAll`, `regexSplit`, `regexExec`, `regexSearch`
253
+ - Validation/predicates: `isEmail`, `isUrl`, `isJson`, `isUuid`, `minLength`, `maxLength`, `lengthEquals`, `isNumber`, `isString`, `isBoolean`, `isArray`, `isObject`, `isEmpty`, `isTruthy`, `isFalsy`, `inRange`, `matchesPattern`, `includes`, `startsWith`, `endsWith`
254
+ - Debug: `log`
255
+
256
+ ## API Reference
257
+
258
+ ### Objector Class
259
+
260
+ #### Constructor
261
+ ```typescript
262
+ const objector = new Objector()
263
+ ```
264
+
265
+ #### Configuration Methods
266
+
267
+ - **`use(handler: (o: Objector) => void): this`**
268
+ Apply a configuration handler
269
+
270
+ - **`includeDirectory(...dirs: string[]): this`**
271
+ Add directories to search for includes
272
+
273
+ - **`include(...files: string[]): this`**
274
+ Add include files directly
275
+
276
+ - **`keys(keys: FieldProcessorMap): this`**
277
+ Add or override key processors
278
+
279
+ - **`sources(sources: FieldProcessorMap): this`**
280
+ Add or override data sources
281
+
282
+ - **`sections(sections: Record<string, FieldProcessorSectionFunc>): this`**
283
+ Register section handlers by type
284
+
285
+ - **`variables(vars: Record<string, any>): this`**
286
+ Set global variables
287
+
288
+ - **`functions(funcs: Record<string, AsyncFunction>): this`**
289
+ Register callable functions for expressions and script sections
290
+
291
+ - **`fieldOptions(options: ProcessFieldsOptions): this`**
292
+ Customize field processing behavior
293
+
294
+ - **`filter(f: RegExp): this`**
295
+ Add a filter for field processing
296
+
297
+ - **`output(o: Output): this`**
298
+ Add an output definition
299
+
300
+ - **`metadata(path: string, value: unknown): this`**
301
+ Store metadata for a path
302
+
303
+ - **`section(section: FieldProcessorSection): this`**
304
+ Register a named section
305
+
306
+ - **`encoders(encoders: Record<string, Encoder>): this`**
307
+ Add output encoders (used by `formatAs`)
308
+
309
+ - **`decoders(decoders: Record<string, LoadFormatParser>): this`**
310
+ Add input decoders for `load()`
311
+
312
+ - **`setCacheMaxSize(size: number): this`**
313
+ Set include/content cache size
314
+
315
+ - **`clearCache(): this`**
316
+ Clear cached loaded content
317
+
318
+ #### Processing Methods
319
+
320
+ - **`async load<T>(fileName: string, cwd?: string, container?: any, noProcess?: boolean): Promise<T | null>`**
321
+ Load and process a file
322
+
323
+ - **`async loadObject<T>(data: T, container?: any, options?: LoadObjectOptions): Promise<T>`**
324
+ Process an object with field processors
325
+
326
+ - **`async writeAll(options?: WriteOutputsOption): Promise<void>`**
327
+ Write all registered outputs to disk
328
+
329
+ #### Query Methods
330
+
331
+ - **`getIncludeDirectories(): string[]`**
332
+ Get all include directories
333
+
334
+ - **`getFunctions(): Record<string, AsyncFunction>`**
335
+ Get registered functions
336
+
337
+ - **`getOutputs(): Output[]`**
338
+ Get all registered outputs
339
+
340
+ - **`getMetadata(path: string): any`**
341
+ Get metadata for a path
342
+
343
+ - **`getAllMetadata(): Record<string, any>`**
344
+ Get all metadata
345
+
346
+ - **`getCurrentContext(): LoadContext | null`**
347
+ Get current processing context
348
+
349
+ - **`getSection(name: string): FieldProcessorSection | undefined`**
350
+ Get a stored section by name
351
+
352
+ - **`getEncoder(name: string): Encoder | undefined`**
353
+ Get an encoder by name
354
+
355
+ - **`getDecoder(name: string): LoadFormatParser | undefined`**
356
+ Get a decoder by name
357
+
358
+ ## Advanced Usage
359
+
360
+ ### Custom Data Sources
361
+
362
+ ```typescript
363
+ import { Objector } from '@homedev/objector'
364
+
365
+ const objector = new Objector()
366
+
367
+ objector.sources({
368
+ // Simple value source
369
+ timestamp: () => Date.now(),
370
+
371
+ // Source with arguments
372
+ add: (ctx) => {
373
+ const [a, b] = ctx.args
374
+ return a + b
375
+ },
376
+
377
+ // Async source
378
+ fetchData: async (ctx) => {
379
+ const url = ctx.args[0]
380
+ const response = await fetch(url)
381
+ return response.json()
382
+ }
383
+ })
384
+ ```
385
+
386
+ **Usage:**
387
+ ```yaml
388
+ data:
389
+ created: $(timestamp)
390
+ sum: $(add:10, 20)
391
+ remote: $(fetchData:"https://api.example.com/data")
392
+ ```
393
+
394
+ ### Custom Key Processors
395
+
396
+ ```typescript
397
+ objector.keys({
398
+ // Custom transformation key
399
+ uppercase: (ctx) => {
400
+ return ctx.value.toUpperCase()
401
+ },
402
+
403
+ // Conditional key
404
+ when: (ctx) => {
405
+ const condition = ctx.value
406
+ if (!condition) {
407
+ return NavigateResult.DeleteParent()
408
+ }
409
+ return NavigateResult.DeleteItem()
410
+ }
411
+ })
412
+ ```
413
+
414
+ **Usage:**
415
+ ```yaml
416
+ settings:
417
+ name:
418
+ .uppercase: hello world
419
+ # Result: "HELLO WORLD"
420
+
421
+ feature:
422
+ .when: $(eq:env, "production")
423
+ enabled: true
424
+ # Only included when condition is true
425
+ ```
426
+
427
+ ### Metadata Collection
428
+
429
+ ```typescript
430
+ const objector = new Objector()
431
+
432
+ // Metadata is collected during processing
433
+ await objector.load('config.yaml')
434
+
435
+ // Retrieve metadata
436
+ const metadata = objector.getMetadata('some.path')
437
+ const allMetadata = objector.getAllMetadata()
438
+ ```
439
+
440
+ **In documents:**
441
+ ```yaml
442
+ service:
443
+ .metadata:
444
+ version: 1.0
445
+ author: team-a
446
+ name: api
447
+ port: 8080
448
+ ```
449
+
450
+ ### Section Blocks (`<@ section ... @>`)
451
+
452
+ Objector can process inline section blocks inside strings. This is useful for generated text files (Markdown, scripts, templates).
453
+
454
+ ### Basic Syntax
455
+
456
+ ```txt
457
+ <@ section[:type] @>
458
+ # optional YAML config
459
+ ---
460
+ # section body
461
+ <@ /section @>
462
+ ```
463
+
464
+ - `:type` is optional. Example: `section:script`.
465
+ - `---` separates YAML config from section content.
466
+ - If no `---` is present, the block is treated as content-only.
467
+
468
+ ### Script Sections
469
+
470
+ `script` is the default built-in section type.
471
+
472
+ ```yaml
473
+ doc: |
474
+ <@ section:script @>
475
+ using: [shout, project]
476
+ condition: context.enabled
477
+ each: context.items
478
+ ---
479
+ section.writeLine(`# ${project}`)
480
+ return shout(String(item))
481
+ <@ /section @>
482
+ ```
483
+
484
+ For `section:script`, the body runs as async JavaScript with access to:
485
+
486
+ - `section`: helper object (`write`, `writeLine`, `prepend`, `clear`, `setOptions`, `getLines`)
487
+ - `context`: current root/context object
488
+ - `config`: section YAML config
489
+ - `system`: current `Objector` instance
490
+ - `item`: current item when `each` is used
491
+ - values listed in `using` (from registered `functions()` or root variables)
492
+
493
+ `condition` can skip rendering, and `each` can iterate an array expression. If the script returns a string, that value becomes the rendered section output.
494
+
495
+ ### Reusing Named Sections
496
+
497
+ You can store a section by name and render it later with `$(section:name)`:
498
+
499
+ ```yaml
500
+ doc: |
501
+ <@ section @>
502
+ name: greeting
503
+ show: true
504
+ ---
505
+ Hello $(user)
506
+ <@ /section @>
507
+
508
+ footer: $(section:greeting)
509
+ ```
510
+
511
+ ## Examples
512
+
513
+ ### Example 1: Multi-Environment Configuration
514
+
515
+ ```yaml
516
+ # base.yaml
517
+ includes:
518
+ - vars-$(env).yaml
519
+
520
+ application:
521
+ name: myapp
522
+ version: 1.0.0
523
+
524
+ database:
525
+ host: $(db.host)
526
+ port: $(db.port)
527
+ name: $(db.name)
528
+
529
+ features:
530
+ - name: caching
531
+ .if: $(eq:env, "production")
532
+ .then:
533
+ enabled: true
534
+ ttl: 3600
535
+ .else:
536
+ enabled: false
537
+
538
+ - name: logging
539
+ level: $(switch:
540
+ from: $(env)
541
+ cases:
542
+ production: warn
543
+ staging: info
544
+ default: debug)
545
+ ```
546
+
547
+ ### Example 2: Service Generation
548
+
549
+ ```yaml
550
+ services:
551
+ .each:
552
+ .from:
553
+ - name: api
554
+ port: 8080
555
+ replicas: 3
556
+ - name: worker
557
+ port: 8081
558
+ replicas: 5
559
+ - name: web
560
+ port: 3000
561
+ replicas: 2
562
+ .model:
563
+ name: $(item.name)
564
+ type: deployment
565
+ spec:
566
+ replicas: $(item.replicas)
567
+ template:
568
+ containers:
569
+ - name: $(item.name)
570
+ image: myapp/$(item.name):$(version)
571
+ ports:
572
+ - containerPort: $(item.port)
573
+ env:
574
+ - name: NODE_ENV
575
+ value: $(env)
576
+ ```
577
+
578
+ ### Example 3: Dynamic Output Generation
579
+
580
+ ```yaml
581
+ outputs:
582
+ .each:
583
+ .from: $(scan:"*.template.yaml", "./templates")
584
+ .model:
585
+ .output:
586
+ file: dist/$(replace:$(item), ".template.yaml", ".yaml")
587
+ content: $(file:$(item))
588
+ ```
589
+
590
+ ## FAQ
591
+
592
+ **Q: How do I debug field processing?**
593
+ A: Use the `$(log:message)` data source to print values during processing.
594
+
595
+ **Q: Can I use async operations in custom sources?**
596
+ A: Yes! All field processors and data sources support async/await.
597
+
598
+ **Q: How do I handle circular includes?**
599
+ A: The system tracks included files to prevent circular dependencies.
600
+
601
+ **Q: Can I extend the built-in processors?**
602
+ A: Yes, use `objector.keys()` and `objector.sources()` to add or override processors.
603
+
604
+ **Q: How do I access nested values?**
605
+ A: Use the selector syntax: `$(some.nested.value)` or `$(@root.some.value)` for root access.
606
+
607
+ **Q: What's the difference between keys and sources?**
608
+ A: Keys are prefixed with `.` and control structure/flow (`.if`, `.each`). Sources use `$()` syntax and provide values (`$(env:VAR)`, `$(file:path)`).