You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/extending.md
+152Lines changed: 152 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -568,3 +568,155 @@ Note the relative paths between these two files.
568
568
569
569
`examples/use_custom_provider.yml`refers to `examples/plugins/tla_provider.py` as `tla_provider.Provider` because the `plugins` folder is in the search path
570
570
described in [How Snowfakery Finds Plugins](#how-snowfakery-finds-plugins).
571
+
572
+
## Adding Validators to Plugins
573
+
574
+
When creating custom plugins, you can add parse-time validators that catch errors before runtime. This allows Snowfakery's `--strict-mode` and `--validate-only` flags to validate your plugin functions.
575
+
576
+
Validators live in a nested `Validators` class alongside the `Functions` class. The validator method name follows the pattern `validate_<function_name>`.
577
+
578
+
### Example: Validator for DoublingPlugin
579
+
580
+
Here's the DoublingPlugin from earlier, now with a validator:
581
+
582
+
```python
583
+
from snowfakery import SnowfakeryPlugin
584
+
585
+
class DoublingPlugin(SnowfakeryPlugin):
586
+
class Functions:
587
+
def double(self, value):
588
+
"""Double a value at runtime."""
589
+
return value * 2
590
+
591
+
class Validators:
592
+
@staticmethod
593
+
def validate_double(sv, context):
594
+
"""Validate double() at parse-time.
595
+
596
+
Args:
597
+
sv: StructuredValue containing args and kwargs from the recipe
598
+
context: ValidationContext for error reporting and value resolution
599
+
600
+
Returns:
601
+
A mock value for continued validation of dependent expressions
602
+
"""
603
+
args = getattr(sv, "args", [])
604
+
605
+
# Check required argument
606
+
if not args:
607
+
context.add_error(
608
+
"double: Missing required argument",
609
+
getattr(sv, "filename", None),
610
+
getattr(sv, "line_num", None),
611
+
)
612
+
return 0 # Return mock value so validation can continue
613
+
614
+
# Return an intelligent mock (doubled value if literal)
615
+
value = args[0]
616
+
if isinstance(value, (int, float)):
617
+
return value * 2
618
+
return 0 # Fallback mock for non-literal values
619
+
```
620
+
621
+
Now when users make mistakes, they get clear error messages. For example, if a user forgets the required argument:
622
+
623
+
```yaml
624
+
Value:
625
+
DoublingPlugin.double:
626
+
```
627
+
628
+
```s
629
+
$ snowfakery recipe.yml --validate-only
630
+
631
+
Validation Errors:
632
+
1. double: Missing required argument
633
+
at recipe.yml:5
634
+
```
635
+
636
+
### Validator Method Signature
637
+
638
+
Every validator follows this pattern:
639
+
640
+
```python
641
+
@staticmethod
642
+
def validate_<function_name>(sv, context):
643
+
"""
644
+
Args:
645
+
sv: StructuredValue with:
646
+
- sv.args: List of positional arguments
647
+
- sv.kwargs: Dict of keyword arguments
648
+
- sv.filename: Source file path
649
+
- sv.line_num: Line number in source file
650
+
651
+
context: ValidationContext with:
652
+
- context.add_error(message, filename, line_num): Report an error
653
+
- context.add_warning(message, filename, line_num): Report a warning
654
+
- context.available_variables: Dict of defined variables
655
+
- context.available_objects: Dict of defined objects
656
+
657
+
Returns:
658
+
A mock value representing what the function would return.
659
+
This allows validation to continue for expressions that use this result.
660
+
"""
661
+
pass
662
+
```
663
+
664
+
### Resolving Values
665
+
666
+
Arguments may be literals, Jinja expressions, or other StructuredValues. Use `resolve_value()` to get the actual value when possible:
667
+
668
+
```python
669
+
from snowfakery.utils.validation_utils import resolve_value
670
+
671
+
class MyPlugin(SnowfakeryPlugin):
672
+
class Validators:
673
+
@staticmethod
674
+
def validate_my_function(sv, context):
675
+
args = getattr(sv, "args", [])
676
+
kwargs = sv.kwargs if hasattr(sv, "kwargs") else {}
677
+
678
+
# Resolve the first argument
679
+
min_val = resolve_value(args[0] if args else kwargs.get("min"), context)
680
+
681
+
# min_val is now a literal (int, str, etc.) or None if unresolvable
682
+
if min_val is not None and not isinstance(min_val, int):
683
+
context.add_error(
684
+
"my_function: 'min' must be an integer",
685
+
getattr(sv, "filename", None),
686
+
getattr(sv, "line_num", None),
687
+
)
688
+
```
689
+
690
+
### Best Practices
691
+
692
+
1. **Always return a mock value** - Even after reporting errors, return a reasonable mock so validation continues for dependent expressions.
Copy file name to clipboardExpand all lines: docs/index.md
+62Lines changed: 62 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -988,6 +988,47 @@ To include a file by a relative path:
988
988
- include_file: child.yml
989
989
```
990
990
991
+
## Recipe Validation
992
+
993
+
Snowfakery can validate recipes before generating data, catching errors like typos, invalid parameters, and undefined variables all at once instead of discovering them one at a time during execution.
994
+
995
+
### Validation Modes
996
+
997
+
| Mode | Flag | Behavior |
998
+
|------|------|----------|
999
+
| **Default** | (none) | No validation; generate data immediately |
1000
+
| **Strict** | `--strict-mode` | Validate first, then generate if no errors |
1001
+
| **Validate Only** | `--validate-only` | Validate and exit; no data generation |
1002
+
1003
+
### Example
1004
+
1005
+
```s
1006
+
$ snowfakery recipe.yml --strict-mode
1007
+
1008
+
Validating recipe...
1009
+
1010
+
✓ Validation passed
1011
+
1012
+
Generating data...
1013
+
Account(id=1, Name=Acme Corp)
1014
+
```
1015
+
1016
+
When errors are found, validation reports them all with precise file locations:
1017
+
1018
+
```s
1019
+
$ snowfakery recipe.yml --strict-mode
1020
+
1021
+
Validating recipe...
1022
+
1023
+
Validation Errors:
1024
+
1. random_number: 'min' (100) must be <= 'max' (50)
1025
+
at recipe.yml:12
1026
+
2. Unknown Faker provider 'frist_name'. Did you mean 'first_name'?
1027
+
at recipe.yml:15
1028
+
```
1029
+
1030
+
To add validators to custom plugins, see [Adding Validators to Plugins](extending.md#adding-validators-to-plugins).
1031
+
991
1032
## Formulas
992
1033
993
1034
To insert data from one field into into another, use a formula.
@@ -1371,6 +1412,12 @@ Options:
1371
1412
--load-declarations FILE Declarations to mix into the generated
1372
1413
mapping file
1373
1414
1415
+
--strict-mode Validate the recipe before generating data.
1416
+
Stops if validation errors are found.
1417
+
1418
+
--validate-only Validate the recipe without generating any
1419
+
data.
1420
+
1374
1421
--version Show the version and exit.
1375
1422
--help Show this message and exit.
1376
1423
```
@@ -1805,12 +1852,27 @@ generate_data(
1805
1852
yaml_file="examples/company.yml",
1806
1853
option=[("A", "B")],
1807
1854
target_number=(20, "Employee"),
1855
+
strict_mode=True, # validate before generating
1808
1856
debug_internals=True,
1809
1857
output_format="json",
1810
1858
output_file=outfile,
1811
1859
)
1812
1860
```
1813
1861
1862
+
To validate without generating data, use `validate_only=True`:
1863
+
1864
+
```python
1865
+
from snowfakery import generate_data
1866
+
1867
+
result = generate_data(
1868
+
yaml_file="examples/company.yml",
1869
+
validate_only=True,
1870
+
)
1871
+
1872
+
if result.has_errors():
1873
+
print(result.get_summary())
1874
+
```
1875
+
1814
1876
To learn more about using Snowfakery in Python, see [Embedding Snowfakery into Python Applications](./embedding.md)
0 commit comments