@constela/core 0.3.1 → 0.3.2

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/dist/index.d.ts CHANGED
@@ -479,7 +479,7 @@ declare function createCondElseRequiredError(path?: string): ConstelaError;
479
479
  * AST Validator for Constela
480
480
  *
481
481
  * This module provides validation for Constela AST structures using
482
- * JSON Schema validation (AJV) and semantic validation.
482
+ * custom validation and semantic validation.
483
483
  */
484
484
 
485
485
  interface ValidationSuccess {
@@ -503,6 +503,7 @@ declare function validateAst(input: unknown): ValidationResult;
503
503
  * JSON Schema for Constela AST
504
504
  *
505
505
  * This module defines the JSON Schema for validating Constela AST structures.
506
+ * The schema is kept for documentation and external tooling purposes.
506
507
  */
507
508
  declare const astSchema: {
508
509
  readonly $schema: "http://json-schema.org/draft-07/schema#";
package/dist/index.js CHANGED
@@ -324,595 +324,193 @@ function createCondElseRequiredError(path) {
324
324
  }
325
325
 
326
326
  // src/schema/validator.ts
327
- import Ajv from "ajv";
328
-
329
- // src/schema/ast.schema.ts
330
- var astSchema = {
331
- $schema: "http://json-schema.org/draft-07/schema#",
332
- $id: "https://constela.dev/schemas/ast.json",
333
- title: "Constela AST",
334
- description: "Schema for Constela UI framework AST",
335
- type: "object",
336
- required: ["version", "state", "actions", "view"],
337
- additionalProperties: false,
338
- properties: {
339
- version: {
340
- type: "string",
341
- const: "1.0"
342
- },
343
- state: {
344
- type: "object",
345
- additionalProperties: {
346
- $ref: "#/$defs/StateField"
327
+ function isObject2(value) {
328
+ return typeof value === "object" && value !== null && !Array.isArray(value);
329
+ }
330
+ var VALID_VIEW_KINDS = ["element", "text", "if", "each", "component", "slot"];
331
+ var VALID_EXPR_TYPES = ["lit", "state", "var", "bin", "not", "param"];
332
+ var VALID_PARAM_TYPES = ["string", "number", "boolean", "json"];
333
+ var VALID_ACTION_TYPES = ["set", "update", "fetch"];
334
+ var VALID_STATE_TYPES = ["number", "string", "list", "boolean", "object"];
335
+ var VALID_BIN_OPS = BINARY_OPERATORS;
336
+ var VALID_UPDATE_OPS = UPDATE_OPERATIONS;
337
+ var VALID_HTTP_METHODS = HTTP_METHODS;
338
+ function validateViewNode(node, path) {
339
+ if (!isObject2(node)) {
340
+ return { path, message: "must be an object" };
341
+ }
342
+ const kind = node["kind"];
343
+ if (typeof kind !== "string") {
344
+ return { path: path + "/kind", message: "kind is required" };
345
+ }
346
+ if (!VALID_VIEW_KINDS.includes(kind)) {
347
+ return { path: path + "/kind", message: "must be one of: element, text, if, each, component, slot" };
348
+ }
349
+ switch (kind) {
350
+ case "element":
351
+ if (typeof node["tag"] !== "string") {
352
+ return { path: path + "/tag", message: "tag is required" };
347
353
  }
348
- },
349
- actions: {
350
- type: "array",
351
- items: {
352
- $ref: "#/$defs/ActionDefinition"
354
+ if (node["props"] !== void 0 && isObject2(node["props"])) {
355
+ for (const [propName, propValue] of Object.entries(node["props"])) {
356
+ if (isObject2(propValue) && "event" in propValue) {
357
+ } else {
358
+ const error = validateExpression(propValue, path + "/props/" + propName);
359
+ if (error) return error;
360
+ }
361
+ }
353
362
  }
354
- },
355
- view: {
356
- $ref: "#/$defs/ViewNode"
357
- },
358
- components: {
359
- type: "object",
360
- additionalProperties: { $ref: "#/$defs/ComponentDef" }
361
- }
362
- },
363
- $defs: {
364
- // ==================== Expressions ====================
365
- Expression: {
366
- oneOf: [
367
- { $ref: "#/$defs/LitExpr" },
368
- { $ref: "#/$defs/StateExpr" },
369
- { $ref: "#/$defs/VarExpr" },
370
- { $ref: "#/$defs/BinExpr" },
371
- { $ref: "#/$defs/NotExpr" },
372
- { $ref: "#/$defs/ParamExpr" },
373
- { $ref: "#/$defs/CondExpr" },
374
- { $ref: "#/$defs/GetExpr" }
375
- ]
376
- },
377
- LitExpr: {
378
- type: "object",
379
- required: ["expr", "value"],
380
- additionalProperties: false,
381
- properties: {
382
- expr: { type: "string", const: "lit" },
383
- value: {
384
- oneOf: [
385
- { type: "string" },
386
- { type: "number" },
387
- { type: "boolean" },
388
- { type: "null" },
389
- { type: "array" }
390
- ]
363
+ if (Array.isArray(node["children"])) {
364
+ for (let i = 0; i < node["children"].length; i++) {
365
+ const error = validateViewNode(node["children"][i], path + "/children/" + i);
366
+ if (error) return error;
391
367
  }
392
368
  }
393
- },
394
- StateExpr: {
395
- type: "object",
396
- required: ["expr", "name"],
397
- additionalProperties: false,
398
- properties: {
399
- expr: { type: "string", const: "state" },
400
- name: { type: "string" }
369
+ break;
370
+ case "text":
371
+ if (!("value" in node)) {
372
+ return { path: path + "/value", message: "value is required" };
401
373
  }
402
- },
403
- VarExpr: {
404
- type: "object",
405
- required: ["expr", "name"],
406
- additionalProperties: false,
407
- properties: {
408
- expr: { type: "string", const: "var" },
409
- name: { type: "string" },
410
- path: { type: "string" }
374
+ return validateExpression(node["value"], path + "/value");
375
+ case "if":
376
+ if (!("condition" in node)) {
377
+ return { path: path + "/condition", message: "condition is required" };
411
378
  }
412
- },
413
- BinExpr: {
414
- type: "object",
415
- required: ["expr", "op", "left", "right"],
416
- additionalProperties: false,
417
- properties: {
418
- expr: { type: "string", const: "bin" },
419
- op: {
420
- type: "string",
421
- enum: ["+", "-", "*", "/", "==", "!=", "<", "<=", ">", ">=", "&&", "||"]
422
- },
423
- left: { $ref: "#/$defs/Expression" },
424
- right: { $ref: "#/$defs/Expression" }
379
+ if (!("then" in node)) {
380
+ return { path: path + "/then", message: "then is required" };
425
381
  }
426
- },
427
- NotExpr: {
428
- type: "object",
429
- required: ["expr", "operand"],
430
- additionalProperties: false,
431
- properties: {
432
- expr: { type: "string", const: "not" },
433
- operand: { $ref: "#/$defs/Expression" }
382
+ {
383
+ const condError = validateExpression(node["condition"], path + "/condition");
384
+ if (condError) return condError;
385
+ const thenError = validateViewNode(node["then"], path + "/then");
386
+ if (thenError) return thenError;
387
+ if ("else" in node) {
388
+ const elseError = validateViewNode(node["else"], path + "/else");
389
+ if (elseError) return elseError;
390
+ }
434
391
  }
435
- },
436
- ParamExpr: {
437
- type: "object",
438
- required: ["expr", "name"],
439
- additionalProperties: false,
440
- properties: {
441
- expr: { type: "string", const: "param" },
442
- name: { type: "string" },
443
- path: { type: "string" }
392
+ break;
393
+ case "each":
394
+ if (!("items" in node)) {
395
+ return { path: path + "/items", message: "items is required" };
444
396
  }
445
- },
446
- CondExpr: {
447
- type: "object",
448
- required: ["expr", "if", "then", "else"],
449
- additionalProperties: false,
450
- properties: {
451
- expr: { type: "string", const: "cond" },
452
- if: { $ref: "#/$defs/Expression" },
453
- then: { $ref: "#/$defs/Expression" },
454
- else: { $ref: "#/$defs/Expression" }
397
+ if (typeof node["as"] !== "string") {
398
+ return { path: path + "/as", message: "as is required" };
455
399
  }
456
- },
457
- GetExpr: {
458
- type: "object",
459
- required: ["expr", "base", "path"],
460
- additionalProperties: false,
461
- properties: {
462
- expr: { type: "string", const: "get" },
463
- base: { $ref: "#/$defs/Expression" },
464
- path: { type: "string" }
400
+ if (!("body" in node)) {
401
+ return { path: path + "/body", message: "body is required" };
465
402
  }
466
- },
467
- // ==================== State Fields ====================
468
- StateField: {
469
- oneOf: [
470
- { $ref: "#/$defs/NumberField" },
471
- { $ref: "#/$defs/StringField" },
472
- { $ref: "#/$defs/ListField" },
473
- { $ref: "#/$defs/BooleanField" },
474
- { $ref: "#/$defs/ObjectField" }
475
- ]
476
- },
477
- NumberField: {
478
- type: "object",
479
- required: ["type", "initial"],
480
- additionalProperties: false,
481
- properties: {
482
- type: { type: "string", const: "number" },
483
- initial: { type: "number" }
403
+ {
404
+ const itemsError = validateExpression(node["items"], path + "/items");
405
+ if (itemsError) return itemsError;
406
+ const bodyError = validateViewNode(node["body"], path + "/body");
407
+ if (bodyError) return bodyError;
484
408
  }
485
- },
486
- StringField: {
487
- type: "object",
488
- required: ["type", "initial"],
489
- additionalProperties: false,
490
- properties: {
491
- type: { type: "string", const: "string" },
492
- initial: { type: "string" }
409
+ break;
410
+ case "component":
411
+ if (typeof node["name"] !== "string") {
412
+ return { path: path + "/name", message: "name is required" };
493
413
  }
494
- },
495
- ListField: {
496
- type: "object",
497
- required: ["type", "initial"],
498
- additionalProperties: false,
499
- properties: {
500
- type: { type: "string", const: "list" },
501
- initial: { type: "array" }
414
+ if (node["props"] !== void 0 && isObject2(node["props"])) {
415
+ for (const [propName, propValue] of Object.entries(node["props"])) {
416
+ const error = validateExpression(propValue, path + "/props/" + propName);
417
+ if (error) return error;
418
+ }
502
419
  }
503
- },
504
- BooleanField: {
505
- type: "object",
506
- required: ["type", "initial"],
507
- additionalProperties: false,
508
- properties: {
509
- type: { type: "string", const: "boolean" },
510
- initial: { type: "boolean" }
420
+ if (Array.isArray(node["children"])) {
421
+ for (let i = 0; i < node["children"].length; i++) {
422
+ const error = validateViewNode(node["children"][i], path + "/children/" + i);
423
+ if (error) return error;
424
+ }
511
425
  }
512
- },
513
- ObjectField: {
514
- type: "object",
515
- required: ["type", "initial"],
516
- additionalProperties: false,
517
- properties: {
518
- type: { type: "string", const: "object" },
519
- initial: { type: "object" }
426
+ break;
427
+ case "slot":
428
+ break;
429
+ }
430
+ return null;
431
+ }
432
+ function validateExpression(expr, path) {
433
+ if (!isObject2(expr)) {
434
+ return { path, message: "must be an object" };
435
+ }
436
+ const exprType = expr["expr"];
437
+ if (typeof exprType !== "string") {
438
+ return { path: path + "/expr", message: "expr is required" };
439
+ }
440
+ if (!VALID_EXPR_TYPES.includes(exprType)) {
441
+ return { path: path + "/expr", message: "must be one of: lit, state, var, bin, not, param" };
442
+ }
443
+ switch (exprType) {
444
+ case "lit":
445
+ if (!("value" in expr)) {
446
+ return { path: path + "/value", message: "value is required" };
520
447
  }
521
- },
522
- // ==================== Action Steps ====================
523
- ActionStep: {
524
- oneOf: [
525
- { $ref: "#/$defs/SetStep" },
526
- { $ref: "#/$defs/UpdateStep" },
527
- { $ref: "#/$defs/FetchStep" }
528
- ]
529
- },
530
- SetStep: {
531
- type: "object",
532
- required: ["do", "target", "value"],
533
- additionalProperties: false,
534
- properties: {
535
- do: { type: "string", const: "set" },
536
- target: { type: "string" },
537
- value: { $ref: "#/$defs/Expression" }
448
+ break;
449
+ case "state":
450
+ case "var":
451
+ if (typeof expr["name"] !== "string") {
452
+ return { path: path + "/name", message: "name is required" };
538
453
  }
539
- },
540
- UpdateStep: {
541
- type: "object",
542
- required: ["do", "target", "operation"],
543
- additionalProperties: false,
544
- properties: {
545
- do: { type: "string", const: "update" },
546
- target: { type: "string" },
547
- operation: {
548
- type: "string",
549
- enum: ["increment", "decrement", "push", "pop", "remove", "toggle", "merge", "replaceAt", "insertAt", "splice"]
550
- },
551
- value: { $ref: "#/$defs/Expression" },
552
- index: { $ref: "#/$defs/Expression" },
553
- deleteCount: { $ref: "#/$defs/Expression" }
454
+ break;
455
+ case "bin":
456
+ if (!("op" in expr)) {
457
+ return { path: path + "/op", message: "op is required" };
554
458
  }
555
- },
556
- FetchStep: {
557
- type: "object",
558
- required: ["do", "url"],
559
- additionalProperties: false,
560
- properties: {
561
- do: { type: "string", const: "fetch" },
562
- url: { $ref: "#/$defs/Expression" },
563
- method: {
564
- type: "string",
565
- enum: ["GET", "POST", "PUT", "DELETE"]
566
- },
567
- body: { $ref: "#/$defs/Expression" },
568
- result: { type: "string" },
569
- onSuccess: {
570
- type: "array",
571
- items: { $ref: "#/$defs/ActionStep" }
572
- },
573
- onError: {
574
- type: "array",
575
- items: { $ref: "#/$defs/ActionStep" }
576
- }
459
+ if (!VALID_BIN_OPS.includes(expr["op"])) {
460
+ return { path: path + "/op", message: "must be a valid operator" };
577
461
  }
578
- },
579
- // ==================== Event Handler ====================
580
- EventHandler: {
581
- type: "object",
582
- required: ["event", "action"],
583
- additionalProperties: false,
584
- properties: {
585
- event: { type: "string" },
586
- action: { type: "string" },
587
- payload: { $ref: "#/$defs/Expression" }
462
+ if (!("left" in expr)) {
463
+ return { path: path + "/left", message: "left is required" };
588
464
  }
589
- },
590
- // ==================== Action Definition ====================
591
- ActionDefinition: {
592
- type: "object",
593
- required: ["name", "steps"],
594
- additionalProperties: false,
595
- properties: {
596
- name: { type: "string" },
597
- steps: {
598
- type: "array",
599
- items: { $ref: "#/$defs/ActionStep" }
600
- }
465
+ if (!("right" in expr)) {
466
+ return { path: path + "/right", message: "right is required" };
601
467
  }
602
- },
603
- // ==================== View Nodes ====================
604
- ViewNode: {
605
- oneOf: [
606
- { $ref: "#/$defs/ElementNode" },
607
- { $ref: "#/$defs/TextNode" },
608
- { $ref: "#/$defs/IfNode" },
609
- { $ref: "#/$defs/EachNode" },
610
- { $ref: "#/$defs/ComponentNode" },
611
- { $ref: "#/$defs/SlotNode" }
612
- ]
613
- },
614
- ElementNode: {
615
- type: "object",
616
- required: ["kind", "tag"],
617
- additionalProperties: false,
618
- properties: {
619
- kind: { type: "string", const: "element" },
620
- tag: { type: "string" },
621
- props: {
622
- type: "object",
623
- additionalProperties: {
624
- oneOf: [
625
- { $ref: "#/$defs/Expression" },
626
- { $ref: "#/$defs/EventHandler" }
627
- ]
628
- }
629
- },
630
- children: {
631
- type: "array",
632
- items: { $ref: "#/$defs/ViewNode" }
633
- }
468
+ {
469
+ const leftError = validateExpression(expr["left"], path + "/left");
470
+ if (leftError) return leftError;
471
+ const rightError = validateExpression(expr["right"], path + "/right");
472
+ if (rightError) return rightError;
634
473
  }
635
- },
636
- TextNode: {
637
- type: "object",
638
- required: ["kind", "value"],
639
- additionalProperties: false,
640
- properties: {
641
- kind: { type: "string", const: "text" },
642
- value: { $ref: "#/$defs/Expression" }
474
+ break;
475
+ case "not":
476
+ if (!("operand" in expr)) {
477
+ return { path: path + "/operand", message: "operand is required" };
643
478
  }
644
- },
645
- IfNode: {
646
- type: "object",
647
- required: ["kind", "condition", "then"],
648
- additionalProperties: false,
649
- properties: {
650
- kind: { type: "string", const: "if" },
651
- condition: { $ref: "#/$defs/Expression" },
652
- then: { $ref: "#/$defs/ViewNode" },
653
- else: { $ref: "#/$defs/ViewNode" }
479
+ return validateExpression(expr["operand"], path + "/operand");
480
+ case "param":
481
+ if (typeof expr["name"] !== "string") {
482
+ return { path: path + "/name", message: "name is required" };
654
483
  }
655
- },
656
- EachNode: {
657
- type: "object",
658
- required: ["kind", "items", "as", "body"],
659
- additionalProperties: false,
660
- properties: {
661
- kind: { type: "string", const: "each" },
662
- items: { $ref: "#/$defs/Expression" },
663
- as: { type: "string" },
664
- index: { type: "string" },
665
- key: { $ref: "#/$defs/Expression" },
666
- body: { $ref: "#/$defs/ViewNode" }
484
+ if ("path" in expr && typeof expr["path"] !== "string") {
485
+ return { path: path + "/path", message: "path must be a string" };
667
486
  }
668
- },
669
- ComponentNode: {
670
- type: "object",
671
- required: ["kind", "name"],
672
- additionalProperties: false,
673
- properties: {
674
- kind: { type: "string", const: "component" },
675
- name: { type: "string" },
676
- props: {
677
- type: "object",
678
- additionalProperties: { $ref: "#/$defs/Expression" }
679
- },
680
- children: {
681
- type: "array",
682
- items: { $ref: "#/$defs/ViewNode" }
683
- }
487
+ break;
488
+ }
489
+ return null;
490
+ }
491
+ function validateActionStep(step, path) {
492
+ if (!isObject2(step)) {
493
+ return { path, message: "must be an object" };
494
+ }
495
+ const doType = step["do"];
496
+ if (typeof doType !== "string") {
497
+ return { path: path + "/do", message: "do is required" };
498
+ }
499
+ if (!VALID_ACTION_TYPES.includes(doType)) {
500
+ return { path: path + "/do", message: "must be one of: set, update, fetch" };
501
+ }
502
+ switch (doType) {
503
+ case "set":
504
+ if (typeof step["target"] !== "string") {
505
+ return { path: path + "/target", message: "target is required" };
684
506
  }
685
- },
686
- SlotNode: {
687
- type: "object",
688
- required: ["kind"],
689
- additionalProperties: false,
690
- properties: {
691
- kind: { type: "string", const: "slot" }
507
+ if (!("value" in step)) {
508
+ return { path: path + "/value", message: "value is required" };
692
509
  }
693
- },
694
- // ==================== Component Definition ====================
695
- ParamDef: {
696
- type: "object",
697
- required: ["type"],
698
- additionalProperties: false,
699
- properties: {
700
- type: {
701
- type: "string",
702
- enum: ["string", "number", "boolean", "json"]
703
- },
704
- required: { type: "boolean" }
705
- }
706
- },
707
- ComponentDef: {
708
- type: "object",
709
- required: ["view"],
710
- additionalProperties: false,
711
- properties: {
712
- params: {
713
- type: "object",
714
- additionalProperties: { $ref: "#/$defs/ParamDef" }
715
- },
716
- view: { $ref: "#/$defs/ViewNode" }
717
- }
718
- }
719
- }
720
- };
721
-
722
- // src/schema/validator.ts
723
- var ajv = new Ajv({
724
- allErrors: true,
725
- verbose: true,
726
- strict: false
727
- });
728
- var validate = ajv.compile(astSchema);
729
- function isObject2(value) {
730
- return typeof value === "object" && value !== null && !Array.isArray(value);
731
- }
732
- var VALID_VIEW_KINDS = ["element", "text", "if", "each", "component", "slot"];
733
- var VALID_EXPR_TYPES = ["lit", "state", "var", "bin", "not", "param"];
734
- var VALID_PARAM_TYPES = ["string", "number", "boolean", "json"];
735
- var VALID_ACTION_TYPES = ["set", "update", "fetch"];
736
- var VALID_STATE_TYPES = ["number", "string", "list", "boolean", "object"];
737
- var VALID_BIN_OPS = BINARY_OPERATORS;
738
- var VALID_UPDATE_OPS = UPDATE_OPERATIONS;
739
- var VALID_HTTP_METHODS = HTTP_METHODS;
740
- function validateViewNode(node, path) {
741
- if (!isObject2(node)) {
742
- return { path, message: "must be an object" };
743
- }
744
- const kind = node["kind"];
745
- if (typeof kind !== "string") {
746
- return { path: path + "/kind", message: "kind is required" };
747
- }
748
- if (!VALID_VIEW_KINDS.includes(kind)) {
749
- return { path: path + "/kind", message: "must be one of: element, text, if, each, component, slot" };
750
- }
751
- switch (kind) {
752
- case "element":
753
- if (typeof node["tag"] !== "string") {
754
- return { path: path + "/tag", message: "tag is required" };
755
- }
756
- if (node["props"] !== void 0 && isObject2(node["props"])) {
757
- for (const [propName, propValue] of Object.entries(node["props"])) {
758
- if (isObject2(propValue) && "event" in propValue) {
759
- } else {
760
- const error = validateExpression(propValue, path + "/props/" + propName);
761
- if (error) return error;
762
- }
763
- }
764
- }
765
- if (Array.isArray(node["children"])) {
766
- for (let i = 0; i < node["children"].length; i++) {
767
- const error = validateViewNode(node["children"][i], path + "/children/" + i);
768
- if (error) return error;
769
- }
770
- }
771
- break;
772
- case "text":
773
- if (!("value" in node)) {
774
- return { path: path + "/value", message: "value is required" };
775
- }
776
- return validateExpression(node["value"], path + "/value");
777
- case "if":
778
- if (!("condition" in node)) {
779
- return { path: path + "/condition", message: "condition is required" };
780
- }
781
- if (!("then" in node)) {
782
- return { path: path + "/then", message: "then is required" };
783
- }
784
- {
785
- const condError = validateExpression(node["condition"], path + "/condition");
786
- if (condError) return condError;
787
- const thenError = validateViewNode(node["then"], path + "/then");
788
- if (thenError) return thenError;
789
- if ("else" in node) {
790
- const elseError = validateViewNode(node["else"], path + "/else");
791
- if (elseError) return elseError;
792
- }
793
- }
794
- break;
795
- case "each":
796
- if (!("items" in node)) {
797
- return { path: path + "/items", message: "items is required" };
798
- }
799
- if (typeof node["as"] !== "string") {
800
- return { path: path + "/as", message: "as is required" };
801
- }
802
- if (!("body" in node)) {
803
- return { path: path + "/body", message: "body is required" };
804
- }
805
- {
806
- const itemsError = validateExpression(node["items"], path + "/items");
807
- if (itemsError) return itemsError;
808
- const bodyError = validateViewNode(node["body"], path + "/body");
809
- if (bodyError) return bodyError;
810
- }
811
- break;
812
- case "component":
813
- if (typeof node["name"] !== "string") {
814
- return { path: path + "/name", message: "name is required" };
815
- }
816
- if (node["props"] !== void 0 && isObject2(node["props"])) {
817
- for (const [propName, propValue] of Object.entries(node["props"])) {
818
- const error = validateExpression(propValue, path + "/props/" + propName);
819
- if (error) return error;
820
- }
821
- }
822
- if (Array.isArray(node["children"])) {
823
- for (let i = 0; i < node["children"].length; i++) {
824
- const error = validateViewNode(node["children"][i], path + "/children/" + i);
825
- if (error) return error;
826
- }
827
- }
828
- break;
829
- case "slot":
830
- break;
831
- }
832
- return null;
833
- }
834
- function validateExpression(expr, path) {
835
- if (!isObject2(expr)) {
836
- return { path, message: "must be an object" };
837
- }
838
- const exprType = expr["expr"];
839
- if (typeof exprType !== "string") {
840
- return { path: path + "/expr", message: "expr is required" };
841
- }
842
- if (!VALID_EXPR_TYPES.includes(exprType)) {
843
- return { path: path + "/expr", message: "must be one of: lit, state, var, bin, not, param" };
844
- }
845
- switch (exprType) {
846
- case "lit":
847
- if (!("value" in expr)) {
848
- return { path: path + "/value", message: "value is required" };
849
- }
850
- break;
851
- case "state":
852
- case "var":
853
- if (typeof expr["name"] !== "string") {
854
- return { path: path + "/name", message: "name is required" };
855
- }
856
- break;
857
- case "bin":
858
- if (!("op" in expr)) {
859
- return { path: path + "/op", message: "op is required" };
860
- }
861
- if (!VALID_BIN_OPS.includes(expr["op"])) {
862
- return { path: path + "/op", message: "must be a valid operator" };
863
- }
864
- if (!("left" in expr)) {
865
- return { path: path + "/left", message: "left is required" };
866
- }
867
- if (!("right" in expr)) {
868
- return { path: path + "/right", message: "right is required" };
869
- }
870
- {
871
- const leftError = validateExpression(expr["left"], path + "/left");
872
- if (leftError) return leftError;
873
- const rightError = validateExpression(expr["right"], path + "/right");
874
- if (rightError) return rightError;
875
- }
876
- break;
877
- case "not":
878
- if (!("operand" in expr)) {
879
- return { path: path + "/operand", message: "operand is required" };
880
- }
881
- return validateExpression(expr["operand"], path + "/operand");
882
- case "param":
883
- if (typeof expr["name"] !== "string") {
884
- return { path: path + "/name", message: "name is required" };
885
- }
886
- if ("path" in expr && typeof expr["path"] !== "string") {
887
- return { path: path + "/path", message: "path must be a string" };
888
- }
889
- break;
890
- }
891
- return null;
892
- }
893
- function validateActionStep(step, path) {
894
- if (!isObject2(step)) {
895
- return { path, message: "must be an object" };
896
- }
897
- const doType = step["do"];
898
- if (typeof doType !== "string") {
899
- return { path: path + "/do", message: "do is required" };
900
- }
901
- if (!VALID_ACTION_TYPES.includes(doType)) {
902
- return { path: path + "/do", message: "must be one of: set, update, fetch" };
903
- }
904
- switch (doType) {
905
- case "set":
906
- if (typeof step["target"] !== "string") {
907
- return { path: path + "/target", message: "target is required" };
908
- }
909
- if (!("value" in step)) {
910
- return { path: path + "/value", message: "value is required" };
911
- }
912
- return validateExpression(step["value"], path + "/value");
913
- case "update":
914
- if (typeof step["target"] !== "string") {
915
- return { path: path + "/target", message: "target is required" };
510
+ return validateExpression(step["value"], path + "/value");
511
+ case "update":
512
+ if (typeof step["target"] !== "string") {
513
+ return { path: path + "/target", message: "target is required" };
916
514
  }
917
515
  if (!("operation" in step)) {
918
516
  return { path: path + "/operation", message: "operation is required" };
@@ -1051,20 +649,6 @@ function customValidateAst(input) {
1051
649
  }
1052
650
  return null;
1053
651
  }
1054
- function findTopLevelRequiredError(errors) {
1055
- for (const error of errors) {
1056
- if (error.keyword === "required" && error.instancePath === "") {
1057
- const params = error.params;
1058
- if (typeof params["missingProperty"] === "string") {
1059
- return {
1060
- path: "/" + params["missingProperty"],
1061
- message: error.message ?? "Required field missing"
1062
- };
1063
- }
1064
- }
1065
- }
1066
- return null;
1067
- }
1068
652
  function collectSemanticContext(ast) {
1069
653
  const stateNames = /* @__PURE__ */ new Set();
1070
654
  const actionNames = /* @__PURE__ */ new Set();
@@ -1200,37 +784,35 @@ function validateAst(input) {
1200
784
  };
1201
785
  }
1202
786
  }
1203
- const valid = validate(input);
1204
- if (!valid && validate.errors && validate.errors.length > 0) {
1205
- const topLevelError = findTopLevelRequiredError(validate.errors);
1206
- if (topLevelError) {
1207
- return {
1208
- ok: false,
1209
- error: createSchemaError(topLevelError.message, topLevelError.path)
1210
- };
1211
- }
1212
- const customError = customValidateAst(input);
1213
- if (customError) {
1214
- return {
1215
- ok: false,
1216
- error: createSchemaError(customError.message, customError.path)
1217
- };
1218
- }
1219
- const firstError = validate.errors[0];
1220
- if (firstError) {
1221
- return {
1222
- ok: false,
1223
- error: createSchemaError(
1224
- firstError.message ?? "Schema validation failed",
1225
- firstError.instancePath || "/"
1226
- )
1227
- };
1228
- }
787
+ if (!("version" in input)) {
788
+ return {
789
+ ok: false,
790
+ error: createSchemaError("must have required property 'version'", "/version")
791
+ };
792
+ }
793
+ if (!("state" in input)) {
794
+ return {
795
+ ok: false,
796
+ error: createSchemaError("must have required property 'state'", "/state")
797
+ };
798
+ }
799
+ if (!("actions" in input)) {
800
+ return {
801
+ ok: false,
802
+ error: createSchemaError("must have required property 'actions'", "/actions")
803
+ };
804
+ }
805
+ if (!("view" in input)) {
806
+ return {
807
+ ok: false,
808
+ error: createSchemaError("must have required property 'view'", "/view")
809
+ };
1229
810
  }
1230
- if (!valid) {
811
+ const customError = customValidateAst(input);
812
+ if (customError) {
1231
813
  return {
1232
814
  ok: false,
1233
- error: createSchemaError("Schema validation failed", "/")
815
+ error: createSchemaError(customError.message, customError.path)
1234
816
  };
1235
817
  }
1236
818
  const semanticError = performSemanticValidation(input);
@@ -1245,6 +827,399 @@ function validateAst(input) {
1245
827
  ast: input
1246
828
  };
1247
829
  }
830
+
831
+ // src/schema/ast.schema.ts
832
+ var astSchema = {
833
+ $schema: "http://json-schema.org/draft-07/schema#",
834
+ $id: "https://constela.dev/schemas/ast.json",
835
+ title: "Constela AST",
836
+ description: "Schema for Constela UI framework AST",
837
+ type: "object",
838
+ required: ["version", "state", "actions", "view"],
839
+ additionalProperties: false,
840
+ properties: {
841
+ version: {
842
+ type: "string",
843
+ const: "1.0"
844
+ },
845
+ state: {
846
+ type: "object",
847
+ additionalProperties: {
848
+ $ref: "#/$defs/StateField"
849
+ }
850
+ },
851
+ actions: {
852
+ type: "array",
853
+ items: {
854
+ $ref: "#/$defs/ActionDefinition"
855
+ }
856
+ },
857
+ view: {
858
+ $ref: "#/$defs/ViewNode"
859
+ },
860
+ components: {
861
+ type: "object",
862
+ additionalProperties: { $ref: "#/$defs/ComponentDef" }
863
+ }
864
+ },
865
+ $defs: {
866
+ // ==================== Expressions ====================
867
+ Expression: {
868
+ oneOf: [
869
+ { $ref: "#/$defs/LitExpr" },
870
+ { $ref: "#/$defs/StateExpr" },
871
+ { $ref: "#/$defs/VarExpr" },
872
+ { $ref: "#/$defs/BinExpr" },
873
+ { $ref: "#/$defs/NotExpr" },
874
+ { $ref: "#/$defs/ParamExpr" },
875
+ { $ref: "#/$defs/CondExpr" },
876
+ { $ref: "#/$defs/GetExpr" }
877
+ ]
878
+ },
879
+ LitExpr: {
880
+ type: "object",
881
+ required: ["expr", "value"],
882
+ additionalProperties: false,
883
+ properties: {
884
+ expr: { type: "string", const: "lit" },
885
+ value: {
886
+ oneOf: [
887
+ { type: "string" },
888
+ { type: "number" },
889
+ { type: "boolean" },
890
+ { type: "null" },
891
+ { type: "array" }
892
+ ]
893
+ }
894
+ }
895
+ },
896
+ StateExpr: {
897
+ type: "object",
898
+ required: ["expr", "name"],
899
+ additionalProperties: false,
900
+ properties: {
901
+ expr: { type: "string", const: "state" },
902
+ name: { type: "string" }
903
+ }
904
+ },
905
+ VarExpr: {
906
+ type: "object",
907
+ required: ["expr", "name"],
908
+ additionalProperties: false,
909
+ properties: {
910
+ expr: { type: "string", const: "var" },
911
+ name: { type: "string" },
912
+ path: { type: "string" }
913
+ }
914
+ },
915
+ BinExpr: {
916
+ type: "object",
917
+ required: ["expr", "op", "left", "right"],
918
+ additionalProperties: false,
919
+ properties: {
920
+ expr: { type: "string", const: "bin" },
921
+ op: {
922
+ type: "string",
923
+ enum: ["+", "-", "*", "/", "==", "!=", "<", "<=", ">", ">=", "&&", "||"]
924
+ },
925
+ left: { $ref: "#/$defs/Expression" },
926
+ right: { $ref: "#/$defs/Expression" }
927
+ }
928
+ },
929
+ NotExpr: {
930
+ type: "object",
931
+ required: ["expr", "operand"],
932
+ additionalProperties: false,
933
+ properties: {
934
+ expr: { type: "string", const: "not" },
935
+ operand: { $ref: "#/$defs/Expression" }
936
+ }
937
+ },
938
+ ParamExpr: {
939
+ type: "object",
940
+ required: ["expr", "name"],
941
+ additionalProperties: false,
942
+ properties: {
943
+ expr: { type: "string", const: "param" },
944
+ name: { type: "string" },
945
+ path: { type: "string" }
946
+ }
947
+ },
948
+ CondExpr: {
949
+ type: "object",
950
+ required: ["expr", "if", "then", "else"],
951
+ additionalProperties: false,
952
+ properties: {
953
+ expr: { type: "string", const: "cond" },
954
+ if: { $ref: "#/$defs/Expression" },
955
+ then: { $ref: "#/$defs/Expression" },
956
+ else: { $ref: "#/$defs/Expression" }
957
+ }
958
+ },
959
+ GetExpr: {
960
+ type: "object",
961
+ required: ["expr", "base", "path"],
962
+ additionalProperties: false,
963
+ properties: {
964
+ expr: { type: "string", const: "get" },
965
+ base: { $ref: "#/$defs/Expression" },
966
+ path: { type: "string" }
967
+ }
968
+ },
969
+ // ==================== State Fields ====================
970
+ StateField: {
971
+ oneOf: [
972
+ { $ref: "#/$defs/NumberField" },
973
+ { $ref: "#/$defs/StringField" },
974
+ { $ref: "#/$defs/ListField" },
975
+ { $ref: "#/$defs/BooleanField" },
976
+ { $ref: "#/$defs/ObjectField" }
977
+ ]
978
+ },
979
+ NumberField: {
980
+ type: "object",
981
+ required: ["type", "initial"],
982
+ additionalProperties: false,
983
+ properties: {
984
+ type: { type: "string", const: "number" },
985
+ initial: { type: "number" }
986
+ }
987
+ },
988
+ StringField: {
989
+ type: "object",
990
+ required: ["type", "initial"],
991
+ additionalProperties: false,
992
+ properties: {
993
+ type: { type: "string", const: "string" },
994
+ initial: { type: "string" }
995
+ }
996
+ },
997
+ ListField: {
998
+ type: "object",
999
+ required: ["type", "initial"],
1000
+ additionalProperties: false,
1001
+ properties: {
1002
+ type: { type: "string", const: "list" },
1003
+ initial: { type: "array" }
1004
+ }
1005
+ },
1006
+ BooleanField: {
1007
+ type: "object",
1008
+ required: ["type", "initial"],
1009
+ additionalProperties: false,
1010
+ properties: {
1011
+ type: { type: "string", const: "boolean" },
1012
+ initial: { type: "boolean" }
1013
+ }
1014
+ },
1015
+ ObjectField: {
1016
+ type: "object",
1017
+ required: ["type", "initial"],
1018
+ additionalProperties: false,
1019
+ properties: {
1020
+ type: { type: "string", const: "object" },
1021
+ initial: { type: "object" }
1022
+ }
1023
+ },
1024
+ // ==================== Action Steps ====================
1025
+ ActionStep: {
1026
+ oneOf: [
1027
+ { $ref: "#/$defs/SetStep" },
1028
+ { $ref: "#/$defs/UpdateStep" },
1029
+ { $ref: "#/$defs/FetchStep" }
1030
+ ]
1031
+ },
1032
+ SetStep: {
1033
+ type: "object",
1034
+ required: ["do", "target", "value"],
1035
+ additionalProperties: false,
1036
+ properties: {
1037
+ do: { type: "string", const: "set" },
1038
+ target: { type: "string" },
1039
+ value: { $ref: "#/$defs/Expression" }
1040
+ }
1041
+ },
1042
+ UpdateStep: {
1043
+ type: "object",
1044
+ required: ["do", "target", "operation"],
1045
+ additionalProperties: false,
1046
+ properties: {
1047
+ do: { type: "string", const: "update" },
1048
+ target: { type: "string" },
1049
+ operation: {
1050
+ type: "string",
1051
+ enum: ["increment", "decrement", "push", "pop", "remove", "toggle", "merge", "replaceAt", "insertAt", "splice"]
1052
+ },
1053
+ value: { $ref: "#/$defs/Expression" },
1054
+ index: { $ref: "#/$defs/Expression" },
1055
+ deleteCount: { $ref: "#/$defs/Expression" }
1056
+ }
1057
+ },
1058
+ FetchStep: {
1059
+ type: "object",
1060
+ required: ["do", "url"],
1061
+ additionalProperties: false,
1062
+ properties: {
1063
+ do: { type: "string", const: "fetch" },
1064
+ url: { $ref: "#/$defs/Expression" },
1065
+ method: {
1066
+ type: "string",
1067
+ enum: ["GET", "POST", "PUT", "DELETE"]
1068
+ },
1069
+ body: { $ref: "#/$defs/Expression" },
1070
+ result: { type: "string" },
1071
+ onSuccess: {
1072
+ type: "array",
1073
+ items: { $ref: "#/$defs/ActionStep" }
1074
+ },
1075
+ onError: {
1076
+ type: "array",
1077
+ items: { $ref: "#/$defs/ActionStep" }
1078
+ }
1079
+ }
1080
+ },
1081
+ // ==================== Event Handler ====================
1082
+ EventHandler: {
1083
+ type: "object",
1084
+ required: ["event", "action"],
1085
+ additionalProperties: false,
1086
+ properties: {
1087
+ event: { type: "string" },
1088
+ action: { type: "string" },
1089
+ payload: { $ref: "#/$defs/Expression" }
1090
+ }
1091
+ },
1092
+ // ==================== Action Definition ====================
1093
+ ActionDefinition: {
1094
+ type: "object",
1095
+ required: ["name", "steps"],
1096
+ additionalProperties: false,
1097
+ properties: {
1098
+ name: { type: "string" },
1099
+ steps: {
1100
+ type: "array",
1101
+ items: { $ref: "#/$defs/ActionStep" }
1102
+ }
1103
+ }
1104
+ },
1105
+ // ==================== View Nodes ====================
1106
+ ViewNode: {
1107
+ oneOf: [
1108
+ { $ref: "#/$defs/ElementNode" },
1109
+ { $ref: "#/$defs/TextNode" },
1110
+ { $ref: "#/$defs/IfNode" },
1111
+ { $ref: "#/$defs/EachNode" },
1112
+ { $ref: "#/$defs/ComponentNode" },
1113
+ { $ref: "#/$defs/SlotNode" }
1114
+ ]
1115
+ },
1116
+ ElementNode: {
1117
+ type: "object",
1118
+ required: ["kind", "tag"],
1119
+ additionalProperties: false,
1120
+ properties: {
1121
+ kind: { type: "string", const: "element" },
1122
+ tag: { type: "string" },
1123
+ props: {
1124
+ type: "object",
1125
+ additionalProperties: {
1126
+ oneOf: [
1127
+ { $ref: "#/$defs/Expression" },
1128
+ { $ref: "#/$defs/EventHandler" }
1129
+ ]
1130
+ }
1131
+ },
1132
+ children: {
1133
+ type: "array",
1134
+ items: { $ref: "#/$defs/ViewNode" }
1135
+ }
1136
+ }
1137
+ },
1138
+ TextNode: {
1139
+ type: "object",
1140
+ required: ["kind", "value"],
1141
+ additionalProperties: false,
1142
+ properties: {
1143
+ kind: { type: "string", const: "text" },
1144
+ value: { $ref: "#/$defs/Expression" }
1145
+ }
1146
+ },
1147
+ IfNode: {
1148
+ type: "object",
1149
+ required: ["kind", "condition", "then"],
1150
+ additionalProperties: false,
1151
+ properties: {
1152
+ kind: { type: "string", const: "if" },
1153
+ condition: { $ref: "#/$defs/Expression" },
1154
+ then: { $ref: "#/$defs/ViewNode" },
1155
+ else: { $ref: "#/$defs/ViewNode" }
1156
+ }
1157
+ },
1158
+ EachNode: {
1159
+ type: "object",
1160
+ required: ["kind", "items", "as", "body"],
1161
+ additionalProperties: false,
1162
+ properties: {
1163
+ kind: { type: "string", const: "each" },
1164
+ items: { $ref: "#/$defs/Expression" },
1165
+ as: { type: "string" },
1166
+ index: { type: "string" },
1167
+ key: { $ref: "#/$defs/Expression" },
1168
+ body: { $ref: "#/$defs/ViewNode" }
1169
+ }
1170
+ },
1171
+ ComponentNode: {
1172
+ type: "object",
1173
+ required: ["kind", "name"],
1174
+ additionalProperties: false,
1175
+ properties: {
1176
+ kind: { type: "string", const: "component" },
1177
+ name: { type: "string" },
1178
+ props: {
1179
+ type: "object",
1180
+ additionalProperties: { $ref: "#/$defs/Expression" }
1181
+ },
1182
+ children: {
1183
+ type: "array",
1184
+ items: { $ref: "#/$defs/ViewNode" }
1185
+ }
1186
+ }
1187
+ },
1188
+ SlotNode: {
1189
+ type: "object",
1190
+ required: ["kind"],
1191
+ additionalProperties: false,
1192
+ properties: {
1193
+ kind: { type: "string", const: "slot" }
1194
+ }
1195
+ },
1196
+ // ==================== Component Definition ====================
1197
+ ParamDef: {
1198
+ type: "object",
1199
+ required: ["type"],
1200
+ additionalProperties: false,
1201
+ properties: {
1202
+ type: {
1203
+ type: "string",
1204
+ enum: ["string", "number", "boolean", "json"]
1205
+ },
1206
+ required: { type: "boolean" }
1207
+ }
1208
+ },
1209
+ ComponentDef: {
1210
+ type: "object",
1211
+ required: ["view"],
1212
+ additionalProperties: false,
1213
+ properties: {
1214
+ params: {
1215
+ type: "object",
1216
+ additionalProperties: { $ref: "#/$defs/ParamDef" }
1217
+ },
1218
+ view: { $ref: "#/$defs/ViewNode" }
1219
+ }
1220
+ }
1221
+ }
1222
+ };
1248
1223
  export {
1249
1224
  BINARY_OPERATORS,
1250
1225
  ConstelaError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/core",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Core types, schema, and validator for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,9 +14,7 @@
14
14
  "files": [
15
15
  "dist"
16
16
  ],
17
- "dependencies": {
18
- "ajv": "^8.12.0"
19
- },
17
+ "dependencies": {},
20
18
  "devDependencies": {
21
19
  "@types/node": "^20.10.0",
22
20
  "tsup": "^8.0.0",