@constela/core 0.3.1 → 0.3.3

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,571 +324,196 @@ 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", "cond", "get"];
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, cond, get" };
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
+ case "cond":
489
+ if (!("if" in expr)) {
490
+ return { path: path + "/if", message: "if is required" };
684
491
  }
685
- },
686
- SlotNode: {
687
- type: "object",
688
- required: ["kind"],
689
- additionalProperties: false,
690
- properties: {
691
- kind: { type: "string", const: "slot" }
492
+ if (!("then" in expr)) {
493
+ return { path: path + "/then", message: "then is required" };
692
494
  }
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" }
495
+ if (!("else" in expr)) {
496
+ return { path: path + "/else", message: "else is required" };
705
497
  }
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" }
498
+ {
499
+ const ifError = validateExpression(expr["if"], path + "/if");
500
+ if (ifError) return ifError;
501
+ const thenError = validateExpression(expr["then"], path + "/then");
502
+ if (thenError) return thenError;
503
+ const elseError = validateExpression(expr["else"], path + "/else");
504
+ if (elseError) return elseError;
717
505
  }
718
- }
506
+ break;
507
+ case "get":
508
+ if (!("base" in expr)) {
509
+ return { path: path + "/base", message: "base is required" };
510
+ }
511
+ if (typeof expr["path"] !== "string") {
512
+ return { path: path + "/path", message: "path is required" };
513
+ }
514
+ return validateExpression(expr["base"], path + "/base");
719
515
  }
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;
516
+ return null;
892
517
  }
893
518
  function validateActionStep(step, path) {
894
519
  if (!isObject2(step)) {
@@ -1051,20 +676,6 @@ function customValidateAst(input) {
1051
676
  }
1052
677
  return null;
1053
678
  }
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
679
  function collectSemanticContext(ast) {
1069
680
  const stateNames = /* @__PURE__ */ new Set();
1070
681
  const actionNames = /* @__PURE__ */ new Set();
@@ -1200,37 +811,35 @@ function validateAst(input) {
1200
811
  };
1201
812
  }
1202
813
  }
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
- }
814
+ if (!("version" in input)) {
815
+ return {
816
+ ok: false,
817
+ error: createSchemaError("must have required property 'version'", "/version")
818
+ };
819
+ }
820
+ if (!("state" in input)) {
821
+ return {
822
+ ok: false,
823
+ error: createSchemaError("must have required property 'state'", "/state")
824
+ };
825
+ }
826
+ if (!("actions" in input)) {
827
+ return {
828
+ ok: false,
829
+ error: createSchemaError("must have required property 'actions'", "/actions")
830
+ };
831
+ }
832
+ if (!("view" in input)) {
833
+ return {
834
+ ok: false,
835
+ error: createSchemaError("must have required property 'view'", "/view")
836
+ };
1229
837
  }
1230
- if (!valid) {
838
+ const customError = customValidateAst(input);
839
+ if (customError) {
1231
840
  return {
1232
841
  ok: false,
1233
- error: createSchemaError("Schema validation failed", "/")
842
+ error: createSchemaError(customError.message, customError.path)
1234
843
  };
1235
844
  }
1236
845
  const semanticError = performSemanticValidation(input);
@@ -1245,6 +854,399 @@ function validateAst(input) {
1245
854
  ast: input
1246
855
  };
1247
856
  }
857
+
858
+ // src/schema/ast.schema.ts
859
+ var astSchema = {
860
+ $schema: "http://json-schema.org/draft-07/schema#",
861
+ $id: "https://constela.dev/schemas/ast.json",
862
+ title: "Constela AST",
863
+ description: "Schema for Constela UI framework AST",
864
+ type: "object",
865
+ required: ["version", "state", "actions", "view"],
866
+ additionalProperties: false,
867
+ properties: {
868
+ version: {
869
+ type: "string",
870
+ const: "1.0"
871
+ },
872
+ state: {
873
+ type: "object",
874
+ additionalProperties: {
875
+ $ref: "#/$defs/StateField"
876
+ }
877
+ },
878
+ actions: {
879
+ type: "array",
880
+ items: {
881
+ $ref: "#/$defs/ActionDefinition"
882
+ }
883
+ },
884
+ view: {
885
+ $ref: "#/$defs/ViewNode"
886
+ },
887
+ components: {
888
+ type: "object",
889
+ additionalProperties: { $ref: "#/$defs/ComponentDef" }
890
+ }
891
+ },
892
+ $defs: {
893
+ // ==================== Expressions ====================
894
+ Expression: {
895
+ oneOf: [
896
+ { $ref: "#/$defs/LitExpr" },
897
+ { $ref: "#/$defs/StateExpr" },
898
+ { $ref: "#/$defs/VarExpr" },
899
+ { $ref: "#/$defs/BinExpr" },
900
+ { $ref: "#/$defs/NotExpr" },
901
+ { $ref: "#/$defs/ParamExpr" },
902
+ { $ref: "#/$defs/CondExpr" },
903
+ { $ref: "#/$defs/GetExpr" }
904
+ ]
905
+ },
906
+ LitExpr: {
907
+ type: "object",
908
+ required: ["expr", "value"],
909
+ additionalProperties: false,
910
+ properties: {
911
+ expr: { type: "string", const: "lit" },
912
+ value: {
913
+ oneOf: [
914
+ { type: "string" },
915
+ { type: "number" },
916
+ { type: "boolean" },
917
+ { type: "null" },
918
+ { type: "array" }
919
+ ]
920
+ }
921
+ }
922
+ },
923
+ StateExpr: {
924
+ type: "object",
925
+ required: ["expr", "name"],
926
+ additionalProperties: false,
927
+ properties: {
928
+ expr: { type: "string", const: "state" },
929
+ name: { type: "string" }
930
+ }
931
+ },
932
+ VarExpr: {
933
+ type: "object",
934
+ required: ["expr", "name"],
935
+ additionalProperties: false,
936
+ properties: {
937
+ expr: { type: "string", const: "var" },
938
+ name: { type: "string" },
939
+ path: { type: "string" }
940
+ }
941
+ },
942
+ BinExpr: {
943
+ type: "object",
944
+ required: ["expr", "op", "left", "right"],
945
+ additionalProperties: false,
946
+ properties: {
947
+ expr: { type: "string", const: "bin" },
948
+ op: {
949
+ type: "string",
950
+ enum: ["+", "-", "*", "/", "==", "!=", "<", "<=", ">", ">=", "&&", "||"]
951
+ },
952
+ left: { $ref: "#/$defs/Expression" },
953
+ right: { $ref: "#/$defs/Expression" }
954
+ }
955
+ },
956
+ NotExpr: {
957
+ type: "object",
958
+ required: ["expr", "operand"],
959
+ additionalProperties: false,
960
+ properties: {
961
+ expr: { type: "string", const: "not" },
962
+ operand: { $ref: "#/$defs/Expression" }
963
+ }
964
+ },
965
+ ParamExpr: {
966
+ type: "object",
967
+ required: ["expr", "name"],
968
+ additionalProperties: false,
969
+ properties: {
970
+ expr: { type: "string", const: "param" },
971
+ name: { type: "string" },
972
+ path: { type: "string" }
973
+ }
974
+ },
975
+ CondExpr: {
976
+ type: "object",
977
+ required: ["expr", "if", "then", "else"],
978
+ additionalProperties: false,
979
+ properties: {
980
+ expr: { type: "string", const: "cond" },
981
+ if: { $ref: "#/$defs/Expression" },
982
+ then: { $ref: "#/$defs/Expression" },
983
+ else: { $ref: "#/$defs/Expression" }
984
+ }
985
+ },
986
+ GetExpr: {
987
+ type: "object",
988
+ required: ["expr", "base", "path"],
989
+ additionalProperties: false,
990
+ properties: {
991
+ expr: { type: "string", const: "get" },
992
+ base: { $ref: "#/$defs/Expression" },
993
+ path: { type: "string" }
994
+ }
995
+ },
996
+ // ==================== State Fields ====================
997
+ StateField: {
998
+ oneOf: [
999
+ { $ref: "#/$defs/NumberField" },
1000
+ { $ref: "#/$defs/StringField" },
1001
+ { $ref: "#/$defs/ListField" },
1002
+ { $ref: "#/$defs/BooleanField" },
1003
+ { $ref: "#/$defs/ObjectField" }
1004
+ ]
1005
+ },
1006
+ NumberField: {
1007
+ type: "object",
1008
+ required: ["type", "initial"],
1009
+ additionalProperties: false,
1010
+ properties: {
1011
+ type: { type: "string", const: "number" },
1012
+ initial: { type: "number" }
1013
+ }
1014
+ },
1015
+ StringField: {
1016
+ type: "object",
1017
+ required: ["type", "initial"],
1018
+ additionalProperties: false,
1019
+ properties: {
1020
+ type: { type: "string", const: "string" },
1021
+ initial: { type: "string" }
1022
+ }
1023
+ },
1024
+ ListField: {
1025
+ type: "object",
1026
+ required: ["type", "initial"],
1027
+ additionalProperties: false,
1028
+ properties: {
1029
+ type: { type: "string", const: "list" },
1030
+ initial: { type: "array" }
1031
+ }
1032
+ },
1033
+ BooleanField: {
1034
+ type: "object",
1035
+ required: ["type", "initial"],
1036
+ additionalProperties: false,
1037
+ properties: {
1038
+ type: { type: "string", const: "boolean" },
1039
+ initial: { type: "boolean" }
1040
+ }
1041
+ },
1042
+ ObjectField: {
1043
+ type: "object",
1044
+ required: ["type", "initial"],
1045
+ additionalProperties: false,
1046
+ properties: {
1047
+ type: { type: "string", const: "object" },
1048
+ initial: { type: "object" }
1049
+ }
1050
+ },
1051
+ // ==================== Action Steps ====================
1052
+ ActionStep: {
1053
+ oneOf: [
1054
+ { $ref: "#/$defs/SetStep" },
1055
+ { $ref: "#/$defs/UpdateStep" },
1056
+ { $ref: "#/$defs/FetchStep" }
1057
+ ]
1058
+ },
1059
+ SetStep: {
1060
+ type: "object",
1061
+ required: ["do", "target", "value"],
1062
+ additionalProperties: false,
1063
+ properties: {
1064
+ do: { type: "string", const: "set" },
1065
+ target: { type: "string" },
1066
+ value: { $ref: "#/$defs/Expression" }
1067
+ }
1068
+ },
1069
+ UpdateStep: {
1070
+ type: "object",
1071
+ required: ["do", "target", "operation"],
1072
+ additionalProperties: false,
1073
+ properties: {
1074
+ do: { type: "string", const: "update" },
1075
+ target: { type: "string" },
1076
+ operation: {
1077
+ type: "string",
1078
+ enum: ["increment", "decrement", "push", "pop", "remove", "toggle", "merge", "replaceAt", "insertAt", "splice"]
1079
+ },
1080
+ value: { $ref: "#/$defs/Expression" },
1081
+ index: { $ref: "#/$defs/Expression" },
1082
+ deleteCount: { $ref: "#/$defs/Expression" }
1083
+ }
1084
+ },
1085
+ FetchStep: {
1086
+ type: "object",
1087
+ required: ["do", "url"],
1088
+ additionalProperties: false,
1089
+ properties: {
1090
+ do: { type: "string", const: "fetch" },
1091
+ url: { $ref: "#/$defs/Expression" },
1092
+ method: {
1093
+ type: "string",
1094
+ enum: ["GET", "POST", "PUT", "DELETE"]
1095
+ },
1096
+ body: { $ref: "#/$defs/Expression" },
1097
+ result: { type: "string" },
1098
+ onSuccess: {
1099
+ type: "array",
1100
+ items: { $ref: "#/$defs/ActionStep" }
1101
+ },
1102
+ onError: {
1103
+ type: "array",
1104
+ items: { $ref: "#/$defs/ActionStep" }
1105
+ }
1106
+ }
1107
+ },
1108
+ // ==================== Event Handler ====================
1109
+ EventHandler: {
1110
+ type: "object",
1111
+ required: ["event", "action"],
1112
+ additionalProperties: false,
1113
+ properties: {
1114
+ event: { type: "string" },
1115
+ action: { type: "string" },
1116
+ payload: { $ref: "#/$defs/Expression" }
1117
+ }
1118
+ },
1119
+ // ==================== Action Definition ====================
1120
+ ActionDefinition: {
1121
+ type: "object",
1122
+ required: ["name", "steps"],
1123
+ additionalProperties: false,
1124
+ properties: {
1125
+ name: { type: "string" },
1126
+ steps: {
1127
+ type: "array",
1128
+ items: { $ref: "#/$defs/ActionStep" }
1129
+ }
1130
+ }
1131
+ },
1132
+ // ==================== View Nodes ====================
1133
+ ViewNode: {
1134
+ oneOf: [
1135
+ { $ref: "#/$defs/ElementNode" },
1136
+ { $ref: "#/$defs/TextNode" },
1137
+ { $ref: "#/$defs/IfNode" },
1138
+ { $ref: "#/$defs/EachNode" },
1139
+ { $ref: "#/$defs/ComponentNode" },
1140
+ { $ref: "#/$defs/SlotNode" }
1141
+ ]
1142
+ },
1143
+ ElementNode: {
1144
+ type: "object",
1145
+ required: ["kind", "tag"],
1146
+ additionalProperties: false,
1147
+ properties: {
1148
+ kind: { type: "string", const: "element" },
1149
+ tag: { type: "string" },
1150
+ props: {
1151
+ type: "object",
1152
+ additionalProperties: {
1153
+ oneOf: [
1154
+ { $ref: "#/$defs/Expression" },
1155
+ { $ref: "#/$defs/EventHandler" }
1156
+ ]
1157
+ }
1158
+ },
1159
+ children: {
1160
+ type: "array",
1161
+ items: { $ref: "#/$defs/ViewNode" }
1162
+ }
1163
+ }
1164
+ },
1165
+ TextNode: {
1166
+ type: "object",
1167
+ required: ["kind", "value"],
1168
+ additionalProperties: false,
1169
+ properties: {
1170
+ kind: { type: "string", const: "text" },
1171
+ value: { $ref: "#/$defs/Expression" }
1172
+ }
1173
+ },
1174
+ IfNode: {
1175
+ type: "object",
1176
+ required: ["kind", "condition", "then"],
1177
+ additionalProperties: false,
1178
+ properties: {
1179
+ kind: { type: "string", const: "if" },
1180
+ condition: { $ref: "#/$defs/Expression" },
1181
+ then: { $ref: "#/$defs/ViewNode" },
1182
+ else: { $ref: "#/$defs/ViewNode" }
1183
+ }
1184
+ },
1185
+ EachNode: {
1186
+ type: "object",
1187
+ required: ["kind", "items", "as", "body"],
1188
+ additionalProperties: false,
1189
+ properties: {
1190
+ kind: { type: "string", const: "each" },
1191
+ items: { $ref: "#/$defs/Expression" },
1192
+ as: { type: "string" },
1193
+ index: { type: "string" },
1194
+ key: { $ref: "#/$defs/Expression" },
1195
+ body: { $ref: "#/$defs/ViewNode" }
1196
+ }
1197
+ },
1198
+ ComponentNode: {
1199
+ type: "object",
1200
+ required: ["kind", "name"],
1201
+ additionalProperties: false,
1202
+ properties: {
1203
+ kind: { type: "string", const: "component" },
1204
+ name: { type: "string" },
1205
+ props: {
1206
+ type: "object",
1207
+ additionalProperties: { $ref: "#/$defs/Expression" }
1208
+ },
1209
+ children: {
1210
+ type: "array",
1211
+ items: { $ref: "#/$defs/ViewNode" }
1212
+ }
1213
+ }
1214
+ },
1215
+ SlotNode: {
1216
+ type: "object",
1217
+ required: ["kind"],
1218
+ additionalProperties: false,
1219
+ properties: {
1220
+ kind: { type: "string", const: "slot" }
1221
+ }
1222
+ },
1223
+ // ==================== Component Definition ====================
1224
+ ParamDef: {
1225
+ type: "object",
1226
+ required: ["type"],
1227
+ additionalProperties: false,
1228
+ properties: {
1229
+ type: {
1230
+ type: "string",
1231
+ enum: ["string", "number", "boolean", "json"]
1232
+ },
1233
+ required: { type: "boolean" }
1234
+ }
1235
+ },
1236
+ ComponentDef: {
1237
+ type: "object",
1238
+ required: ["view"],
1239
+ additionalProperties: false,
1240
+ properties: {
1241
+ params: {
1242
+ type: "object",
1243
+ additionalProperties: { $ref: "#/$defs/ParamDef" }
1244
+ },
1245
+ view: { $ref: "#/$defs/ViewNode" }
1246
+ }
1247
+ }
1248
+ }
1249
+ };
1248
1250
  export {
1249
1251
  BINARY_OPERATORS,
1250
1252
  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.3",
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",