wizard-codegen 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- wizard_codegen-0.2.1/README.md → wizard_codegen-0.3.0/PKG-INFO +188 -3
- wizard_codegen-0.2.1/PKG-INFO → wizard_codegen-0.3.0/README.md +171 -18
- wizard_codegen-0.3.0/cli/context_explorer/__init__.py +5 -0
- wizard_codegen-0.3.0/cli/context_explorer/path_resolver.py +118 -0
- wizard_codegen-0.3.0/cli/context_explorer/repl.py +319 -0
- wizard_codegen-0.3.0/cli/context_explorer/tree.py +164 -0
- wizard_codegen-0.3.0/cli/context_explorer/tui.py +591 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/cli/main.py +121 -23
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/__init__.py +5 -2
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/filter.py +5 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/renderer.py +9 -2
- wizard_codegen-0.3.0/core/samples.py +347 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/writer.py +1 -2
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/pyproject.toml +6 -3
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/.gitignore +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/cli/__init__.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/config.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/context_builder.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/hooks/__init__.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/hooks/hooks.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/__init__.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/discover.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/fds_loader.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/proto_source.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/protoc_runner.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/tests/fixtures/hooks/__init__.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/tests/fixtures/hooks/type_mapping.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/__init__.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/name.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/options.py +0 -0
- {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/path.py +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wizard-codegen
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A powerful, template-driven code generation tool for Protocol Buffers
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: click==8.3.1
|
|
7
|
+
Requires-Dist: jinja2==3.1.6
|
|
8
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
9
|
+
Requires-Dist: protobuf==6.33.5
|
|
10
|
+
Requires-Dist: pydantic-core==2.41.5
|
|
11
|
+
Requires-Dist: pydantic==2.12.5
|
|
12
|
+
Requires-Dist: pyyaml==6.0.3
|
|
13
|
+
Requires-Dist: rich==14.3.3
|
|
14
|
+
Requires-Dist: textual>=8.1.1
|
|
15
|
+
Requires-Dist: typer==0.21.1
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
1
18
|
# 🧙 Wizard Codegen
|
|
2
19
|
|
|
3
20
|
[](https://dl.circleci.com/status-badge/redirect/gh/ConsultingMD/wizard-codegen/tree/main)
|
|
@@ -46,6 +63,7 @@ handlers — this tool has you covered.
|
|
|
46
63
|
| **Multi-Language Support** | Generate code for TypeScript, Swift, Kotlin, Go, and more |
|
|
47
64
|
| **Jinja2 Templates** | Full power of Jinja2 templating with custom filters |
|
|
48
65
|
| **Flexible Filtering** | Target specific messages, enums, or services with `where` clauses |
|
|
66
|
+
| **Runtime Name Filter** | Generate only specific items from CLI: `wizard-codegen generate SampleProtoPage` |
|
|
49
67
|
| **Annotation Filtering** | Filter messages by custom proto annotations/options (e.g., `has_option: "*.page"`) |
|
|
50
68
|
| **Git Proto Sources** | Fetch proto definitions directly from Git repositories |
|
|
51
69
|
| **Multiple Write Modes** | `overwrite`, `append`, or `write-once` file generation |
|
|
@@ -55,6 +73,9 @@ handlers — this tool has you covered.
|
|
|
55
73
|
| **Verbose Output** | Debug with detailed proto and context inspection |
|
|
56
74
|
| **Nested Type Support** | Full support for nested messages and enums |
|
|
57
75
|
| **Options Extraction** | Access message, field, and enum options in templates |
|
|
76
|
+
| **Sample Data Loading** | Auto-load `.samples.json` files co-located with protos into template context (`item.samples`, `item.default_sample`) |
|
|
77
|
+
| **Sample Validation** | `validate` command checks `.samples.json` field names and types against proto schema recursively |
|
|
78
|
+
| **Context Explorer** | Interactive TUI and REPL for exploring the Jinja2 template context (`context-explorer` command) |
|
|
58
79
|
|
|
59
80
|
---
|
|
60
81
|
|
|
@@ -195,9 +216,18 @@ The CLI is built with [Typer](https://typer.tiangolo.com/) and provides rich, co
|
|
|
195
216
|
#### `generate` — Generate code from protos
|
|
196
217
|
|
|
197
218
|
```bash
|
|
198
|
-
# Basic generation
|
|
219
|
+
# Basic generation (all items)
|
|
199
220
|
wizard-codegen generate
|
|
200
221
|
|
|
222
|
+
# Generate only specific items by name
|
|
223
|
+
wizard-codegen generate SampleProtoPage
|
|
224
|
+
|
|
225
|
+
# Generate multiple specific items
|
|
226
|
+
wizard-codegen generate SampleProtoPage UserFormPage
|
|
227
|
+
|
|
228
|
+
# Use glob patterns to match item names
|
|
229
|
+
wizard-codegen generate "*Page"
|
|
230
|
+
|
|
201
231
|
# With custom config file
|
|
202
232
|
wizard-codegen --config path/to/codegen.yaml generate
|
|
203
233
|
|
|
@@ -207,10 +237,12 @@ wizard-codegen --dry-run generate
|
|
|
207
237
|
# Verbose mode (detailed output)
|
|
208
238
|
wizard-codegen --verbose generate
|
|
209
239
|
|
|
210
|
-
# Combine flags
|
|
211
|
-
wizard-codegen --verbose --dry-run
|
|
240
|
+
# Combine flags and name filter
|
|
241
|
+
wizard-codegen --verbose --dry-run generate SampleProtoPage
|
|
212
242
|
```
|
|
213
243
|
|
|
244
|
+
The optional positional `NAMES` arguments filter which items are generated at runtime. This works alongside any `where` clauses in your config — the config filters are applied first, then the name filter narrows the results further. Glob patterns (`*Page`, `Sample*`) and regex patterns are supported. When no names are provided, all matching items are generated as usual.
|
|
245
|
+
|
|
214
246
|
#### `list-protos` — List discovered proto files
|
|
215
247
|
|
|
216
248
|
```bash
|
|
@@ -486,6 +518,8 @@ When a template is rendered, it receives a rich context:
|
|
|
486
518
|
"fields": [...], # List of field objects
|
|
487
519
|
"nested_messages": [...], # Nested message definitions
|
|
488
520
|
"nested_enums": [...], # Nested enum definitions
|
|
521
|
+
"samples": [...], # Loaded from co-located .samples.json (empty list if none)
|
|
522
|
+
"default_sample": {...}, # Resolved default sample (None if no samples)
|
|
489
523
|
},
|
|
490
524
|
|
|
491
525
|
# All files (for cross-referencing)
|
|
@@ -588,6 +622,157 @@ When a template is rendered, it receives a rich context:
|
|
|
588
622
|
}
|
|
589
623
|
```
|
|
590
624
|
|
|
625
|
+
### Sample Data (`.samples.json`)
|
|
626
|
+
|
|
627
|
+
Proto files can have a co-located `.samples.json` file that provides example data. The file mirrors the proto filename:
|
|
628
|
+
|
|
629
|
+
```
|
|
630
|
+
service/wizard/pages/sample/v1/
|
|
631
|
+
sample.proto # the page definition
|
|
632
|
+
sample.samples.json # sample data
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
When found, the samples are loaded into the template context on the matching message item. Templates can access them via `item.samples` (always a list) and `item.default_sample` (the resolved default, or `None`).
|
|
636
|
+
|
|
637
|
+
#### Single sample
|
|
638
|
+
|
|
639
|
+
```json
|
|
640
|
+
{
|
|
641
|
+
"context": {
|
|
642
|
+
"header": { "title": "Welcome" },
|
|
643
|
+
"primaryButtonText": "Continue"
|
|
644
|
+
},
|
|
645
|
+
"form": { "inputFieldValue": "" }
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
#### Multiple samples
|
|
650
|
+
|
|
651
|
+
Use an array. Name one `"default"` to mark it as the default; otherwise the first entry is used.
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
[
|
|
655
|
+
{ "name": "default", "context": { "header": { "title": "Welcome" } }, "form": {} },
|
|
656
|
+
{ "name": "returning_user", "context": { "header": { "title": "Welcome back" } }, "form": {} }
|
|
657
|
+
]
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
#### Using samples in templates
|
|
661
|
+
|
|
662
|
+
```jinja
|
|
663
|
+
{% if item.default_sample %}
|
|
664
|
+
var defaultContext = {{ item.default_sample.context | tojson }};
|
|
665
|
+
{% endif %}
|
|
666
|
+
|
|
667
|
+
{% for sample in item.samples %}
|
|
668
|
+
// Sample: {{ sample.name | default("unnamed") }}
|
|
669
|
+
{% endfor %}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
#### Validation
|
|
673
|
+
|
|
674
|
+
The `wizard-codegen validate` command checks all found `.samples.json` files:
|
|
675
|
+
|
|
676
|
+
- Invalid JSON is reported with parse details
|
|
677
|
+
- Empty arrays are rejected
|
|
678
|
+
- Every JSON key is checked against the message's proto field `json_name` values
|
|
679
|
+
- Value types are checked (string, number, bool, object, array) against proto field types
|
|
680
|
+
- Message-type fields are validated recursively, including externally referenced messages
|
|
681
|
+
- Repeated fields must be arrays with correctly typed elements
|
|
682
|
+
|
|
683
|
+
Samples are **optional** — the `generate` command works with or without them, and `validate` only checks files that exist (no warning for missing samples).
|
|
684
|
+
|
|
685
|
+
#### Nested message propagation
|
|
686
|
+
|
|
687
|
+
Samples are automatically propagated from a top-level message to its nested message types. For example, given a wizard page with nested `Context` and `Form` messages and a sample containing `{"context": {...}, "form": {...}}`, the `Context` message receives the `context` sub-object as its sample, and `Form` receives the `form` sub-object. The matching is based on the camelCase type name of the nested message. This means `item.samples` and `item.default_sample` are available in templates for both the parent and its nested message types.
|
|
688
|
+
|
|
689
|
+
### Context Explorer
|
|
690
|
+
|
|
691
|
+
The `context-explorer` command helps template authors discover what data is available in the Jinja2 template context. It supports two modes.
|
|
692
|
+
|
|
693
|
+
#### Interactive TUI (default)
|
|
694
|
+
|
|
695
|
+
The default mode launches a full-screen interactive terminal UI with an expandable/collapsible tree on the left and a detail inspector panel on the right:
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# Launch TUI
|
|
699
|
+
wizard-codegen context-explorer
|
|
700
|
+
|
|
701
|
+
# Auto-expand to a specific path
|
|
702
|
+
wizard-codegen context-explorer "message[.service.wizard.pages.sample.v1.SampleProtoPage]"
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```
|
|
706
|
+
+-------------------------------+-------------------------------+
|
|
707
|
+
| Context Explorer | |
|
|
708
|
+
+-------------------------------+-------------------------------+
|
|
709
|
+
| Context | Path: message[.pkg.Foo].name |
|
|
710
|
+
| v proto_root: "/path/..." | |
|
|
711
|
+
| v files (3 items) | Type: Name |
|
|
712
|
+
| > [0] sample_page.proto | |
|
|
713
|
+
| > [1] other.proto | raw SampleProtoPage |
|
|
714
|
+
| v message (5 keys) | snake_case sample_proto_page |
|
|
715
|
+
| v .pkg.SampleProtoPage | kebab_case sample-proto-page |
|
|
716
|
+
| > name: SampleProtoPage | pascal_case SampleProtoPage |
|
|
717
|
+
| > fields (3 items) | camel_case sampleProtoPage |
|
|
718
|
+
| > nested_messages (2) | macro_case SAMPLEPROTOPAGE |
|
|
719
|
+
| > options (1 keys) | macro_snake SAMPLE_PROTO_PAGE |
|
|
720
|
+
| > .pkg.Other | micro_case sampleprotopage |
|
|
721
|
+
| > enum (2 keys) | |
|
|
722
|
+
| > service (1 keys) | |
|
|
723
|
+
+-------------------------------+-------------------------------+
|
|
724
|
+
| q Quit | space Toggle | arrows Navigate |
|
|
725
|
+
+-------------------------------+-------------------------------+
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Nodes start collapsed and are lazily loaded on expand — keeping startup fast even for large contexts. Use arrow keys to navigate, space/enter to expand/collapse, `/` to focus the filter input, and `q` to quit.
|
|
729
|
+
|
|
730
|
+
The filter input at the top lets you narrow down dict keys across all top-level sections (message, enum, service, types). Type a substring (e.g. `sample`) to hide keys that don't match, and press Escape to clear the filter. This is especially useful for hiding built-in google protobuf definitions.
|
|
731
|
+
|
|
732
|
+
#### REPL mode
|
|
733
|
+
|
|
734
|
+
Launch with `--repl` / `-r` for step-by-step text navigation with tab completion:
|
|
735
|
+
|
|
736
|
+
```bash
|
|
737
|
+
wizard-codegen context-explorer -r
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
Context Explorer - type help for commands
|
|
742
|
+
|
|
743
|
+
context:/> message
|
|
744
|
+
Key Type Preview
|
|
745
|
+
.service.wizard.pages.sample.v1.SampleProtoPage dict (8 keys) name, full_name, fields, ...
|
|
746
|
+
|
|
747
|
+
context:/message> [.service.wizard.pages.sample.v1.SampleProtoPage]
|
|
748
|
+
Key Type Preview
|
|
749
|
+
name Name SampleProtoPage
|
|
750
|
+
full_name str .service.wizard.pages.sample.v1.SampleProtoPage
|
|
751
|
+
fields list (0 items)
|
|
752
|
+
nested_messages list (2 items)
|
|
753
|
+
samples list (2 items)
|
|
754
|
+
default_sample dict (3 keys) name, context, form
|
|
755
|
+
|
|
756
|
+
context:/message[.service.wizard.pages.sample.v1.SampleProtoPage]> name
|
|
757
|
+
SampleProtoPage (raw=SampleProtoPage, snake=sample_proto_page, kebab=sample-proto-page,
|
|
758
|
+
pascal=SampleProtoPage, camel=sampleProtoPage, macro=SAMPLEPROTOPAGE,
|
|
759
|
+
macro_snake=SAMPLE_PROTO_PAGE, micro=sampleprotopage)
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
REPL commands:
|
|
763
|
+
|
|
764
|
+
| Command | Description |
|
|
765
|
+
|----------------|----------------------------------------------------------------|
|
|
766
|
+
| `<path>` | Navigate to path (dot-separated, `[brackets]` for dict/list) |
|
|
767
|
+
| `keys` | Show available keys at current level |
|
|
768
|
+
| `..` | Go up one level |
|
|
769
|
+
| `/` | Go to root |
|
|
770
|
+
| `tree [depth]` | Show tree from current position (optional depth limit) |
|
|
771
|
+
| `help` | Show help |
|
|
772
|
+
| `quit` | Exit the explorer |
|
|
773
|
+
|
|
774
|
+
Path syntax uses dots for plain keys and brackets for keys containing dots or list indices: `message[.pkg.Foo].fields[0].name.snake_case`.
|
|
775
|
+
|
|
591
776
|
### Name Transformations
|
|
592
777
|
|
|
593
778
|
Every name in the context is wrapped in a `Name` object that provides automatic case transformations:
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: wizard-codegen
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: A powerful, template-driven code generation tool for Protocol Buffers
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: click==8.3.1
|
|
7
|
-
Requires-Dist: jinja2==3.1.6
|
|
8
|
-
Requires-Dist: protobuf==6.33.4
|
|
9
|
-
Requires-Dist: pydantic-core==2.41.5
|
|
10
|
-
Requires-Dist: pydantic==2.12.5
|
|
11
|
-
Requires-Dist: pyyaml==6.0.3
|
|
12
|
-
Requires-Dist: rich==14.3.2
|
|
13
|
-
Requires-Dist: typer==0.21.1
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
1
|
# 🧙 Wizard Codegen
|
|
17
2
|
|
|
18
3
|
[](https://dl.circleci.com/status-badge/redirect/gh/ConsultingMD/wizard-codegen/tree/main)
|
|
@@ -61,6 +46,7 @@ handlers — this tool has you covered.
|
|
|
61
46
|
| **Multi-Language Support** | Generate code for TypeScript, Swift, Kotlin, Go, and more |
|
|
62
47
|
| **Jinja2 Templates** | Full power of Jinja2 templating with custom filters |
|
|
63
48
|
| **Flexible Filtering** | Target specific messages, enums, or services with `where` clauses |
|
|
49
|
+
| **Runtime Name Filter** | Generate only specific items from CLI: `wizard-codegen generate SampleProtoPage` |
|
|
64
50
|
| **Annotation Filtering** | Filter messages by custom proto annotations/options (e.g., `has_option: "*.page"`) |
|
|
65
51
|
| **Git Proto Sources** | Fetch proto definitions directly from Git repositories |
|
|
66
52
|
| **Multiple Write Modes** | `overwrite`, `append`, or `write-once` file generation |
|
|
@@ -70,6 +56,9 @@ handlers — this tool has you covered.
|
|
|
70
56
|
| **Verbose Output** | Debug with detailed proto and context inspection |
|
|
71
57
|
| **Nested Type Support** | Full support for nested messages and enums |
|
|
72
58
|
| **Options Extraction** | Access message, field, and enum options in templates |
|
|
59
|
+
| **Sample Data Loading** | Auto-load `.samples.json` files co-located with protos into template context (`item.samples`, `item.default_sample`) |
|
|
60
|
+
| **Sample Validation** | `validate` command checks `.samples.json` field names and types against proto schema recursively |
|
|
61
|
+
| **Context Explorer** | Interactive TUI and REPL for exploring the Jinja2 template context (`context-explorer` command) |
|
|
73
62
|
|
|
74
63
|
---
|
|
75
64
|
|
|
@@ -210,9 +199,18 @@ The CLI is built with [Typer](https://typer.tiangolo.com/) and provides rich, co
|
|
|
210
199
|
#### `generate` — Generate code from protos
|
|
211
200
|
|
|
212
201
|
```bash
|
|
213
|
-
# Basic generation
|
|
202
|
+
# Basic generation (all items)
|
|
214
203
|
wizard-codegen generate
|
|
215
204
|
|
|
205
|
+
# Generate only specific items by name
|
|
206
|
+
wizard-codegen generate SampleProtoPage
|
|
207
|
+
|
|
208
|
+
# Generate multiple specific items
|
|
209
|
+
wizard-codegen generate SampleProtoPage UserFormPage
|
|
210
|
+
|
|
211
|
+
# Use glob patterns to match item names
|
|
212
|
+
wizard-codegen generate "*Page"
|
|
213
|
+
|
|
216
214
|
# With custom config file
|
|
217
215
|
wizard-codegen --config path/to/codegen.yaml generate
|
|
218
216
|
|
|
@@ -222,10 +220,12 @@ wizard-codegen --dry-run generate
|
|
|
222
220
|
# Verbose mode (detailed output)
|
|
223
221
|
wizard-codegen --verbose generate
|
|
224
222
|
|
|
225
|
-
# Combine flags
|
|
226
|
-
wizard-codegen --verbose --dry-run
|
|
223
|
+
# Combine flags and name filter
|
|
224
|
+
wizard-codegen --verbose --dry-run generate SampleProtoPage
|
|
227
225
|
```
|
|
228
226
|
|
|
227
|
+
The optional positional `NAMES` arguments filter which items are generated at runtime. This works alongside any `where` clauses in your config — the config filters are applied first, then the name filter narrows the results further. Glob patterns (`*Page`, `Sample*`) and regex patterns are supported. When no names are provided, all matching items are generated as usual.
|
|
228
|
+
|
|
229
229
|
#### `list-protos` — List discovered proto files
|
|
230
230
|
|
|
231
231
|
```bash
|
|
@@ -501,6 +501,8 @@ When a template is rendered, it receives a rich context:
|
|
|
501
501
|
"fields": [...], # List of field objects
|
|
502
502
|
"nested_messages": [...], # Nested message definitions
|
|
503
503
|
"nested_enums": [...], # Nested enum definitions
|
|
504
|
+
"samples": [...], # Loaded from co-located .samples.json (empty list if none)
|
|
505
|
+
"default_sample": {...}, # Resolved default sample (None if no samples)
|
|
504
506
|
},
|
|
505
507
|
|
|
506
508
|
# All files (for cross-referencing)
|
|
@@ -603,6 +605,157 @@ When a template is rendered, it receives a rich context:
|
|
|
603
605
|
}
|
|
604
606
|
```
|
|
605
607
|
|
|
608
|
+
### Sample Data (`.samples.json`)
|
|
609
|
+
|
|
610
|
+
Proto files can have a co-located `.samples.json` file that provides example data. The file mirrors the proto filename:
|
|
611
|
+
|
|
612
|
+
```
|
|
613
|
+
service/wizard/pages/sample/v1/
|
|
614
|
+
sample.proto # the page definition
|
|
615
|
+
sample.samples.json # sample data
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
When found, the samples are loaded into the template context on the matching message item. Templates can access them via `item.samples` (always a list) and `item.default_sample` (the resolved default, or `None`).
|
|
619
|
+
|
|
620
|
+
#### Single sample
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"context": {
|
|
625
|
+
"header": { "title": "Welcome" },
|
|
626
|
+
"primaryButtonText": "Continue"
|
|
627
|
+
},
|
|
628
|
+
"form": { "inputFieldValue": "" }
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### Multiple samples
|
|
633
|
+
|
|
634
|
+
Use an array. Name one `"default"` to mark it as the default; otherwise the first entry is used.
|
|
635
|
+
|
|
636
|
+
```json
|
|
637
|
+
[
|
|
638
|
+
{ "name": "default", "context": { "header": { "title": "Welcome" } }, "form": {} },
|
|
639
|
+
{ "name": "returning_user", "context": { "header": { "title": "Welcome back" } }, "form": {} }
|
|
640
|
+
]
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### Using samples in templates
|
|
644
|
+
|
|
645
|
+
```jinja
|
|
646
|
+
{% if item.default_sample %}
|
|
647
|
+
var defaultContext = {{ item.default_sample.context | tojson }};
|
|
648
|
+
{% endif %}
|
|
649
|
+
|
|
650
|
+
{% for sample in item.samples %}
|
|
651
|
+
// Sample: {{ sample.name | default("unnamed") }}
|
|
652
|
+
{% endfor %}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### Validation
|
|
656
|
+
|
|
657
|
+
The `wizard-codegen validate` command checks all found `.samples.json` files:
|
|
658
|
+
|
|
659
|
+
- Invalid JSON is reported with parse details
|
|
660
|
+
- Empty arrays are rejected
|
|
661
|
+
- Every JSON key is checked against the message's proto field `json_name` values
|
|
662
|
+
- Value types are checked (string, number, bool, object, array) against proto field types
|
|
663
|
+
- Message-type fields are validated recursively, including externally referenced messages
|
|
664
|
+
- Repeated fields must be arrays with correctly typed elements
|
|
665
|
+
|
|
666
|
+
Samples are **optional** — the `generate` command works with or without them, and `validate` only checks files that exist (no warning for missing samples).
|
|
667
|
+
|
|
668
|
+
#### Nested message propagation
|
|
669
|
+
|
|
670
|
+
Samples are automatically propagated from a top-level message to its nested message types. For example, given a wizard page with nested `Context` and `Form` messages and a sample containing `{"context": {...}, "form": {...}}`, the `Context` message receives the `context` sub-object as its sample, and `Form` receives the `form` sub-object. The matching is based on the camelCase type name of the nested message. This means `item.samples` and `item.default_sample` are available in templates for both the parent and its nested message types.
|
|
671
|
+
|
|
672
|
+
### Context Explorer
|
|
673
|
+
|
|
674
|
+
The `context-explorer` command helps template authors discover what data is available in the Jinja2 template context. It supports two modes.
|
|
675
|
+
|
|
676
|
+
#### Interactive TUI (default)
|
|
677
|
+
|
|
678
|
+
The default mode launches a full-screen interactive terminal UI with an expandable/collapsible tree on the left and a detail inspector panel on the right:
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# Launch TUI
|
|
682
|
+
wizard-codegen context-explorer
|
|
683
|
+
|
|
684
|
+
# Auto-expand to a specific path
|
|
685
|
+
wizard-codegen context-explorer "message[.service.wizard.pages.sample.v1.SampleProtoPage]"
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
```
|
|
689
|
+
+-------------------------------+-------------------------------+
|
|
690
|
+
| Context Explorer | |
|
|
691
|
+
+-------------------------------+-------------------------------+
|
|
692
|
+
| Context | Path: message[.pkg.Foo].name |
|
|
693
|
+
| v proto_root: "/path/..." | |
|
|
694
|
+
| v files (3 items) | Type: Name |
|
|
695
|
+
| > [0] sample_page.proto | |
|
|
696
|
+
| > [1] other.proto | raw SampleProtoPage |
|
|
697
|
+
| v message (5 keys) | snake_case sample_proto_page |
|
|
698
|
+
| v .pkg.SampleProtoPage | kebab_case sample-proto-page |
|
|
699
|
+
| > name: SampleProtoPage | pascal_case SampleProtoPage |
|
|
700
|
+
| > fields (3 items) | camel_case sampleProtoPage |
|
|
701
|
+
| > nested_messages (2) | macro_case SAMPLEPROTOPAGE |
|
|
702
|
+
| > options (1 keys) | macro_snake SAMPLE_PROTO_PAGE |
|
|
703
|
+
| > .pkg.Other | micro_case sampleprotopage |
|
|
704
|
+
| > enum (2 keys) | |
|
|
705
|
+
| > service (1 keys) | |
|
|
706
|
+
+-------------------------------+-------------------------------+
|
|
707
|
+
| q Quit | space Toggle | arrows Navigate |
|
|
708
|
+
+-------------------------------+-------------------------------+
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Nodes start collapsed and are lazily loaded on expand — keeping startup fast even for large contexts. Use arrow keys to navigate, space/enter to expand/collapse, `/` to focus the filter input, and `q` to quit.
|
|
712
|
+
|
|
713
|
+
The filter input at the top lets you narrow down dict keys across all top-level sections (message, enum, service, types). Type a substring (e.g. `sample`) to hide keys that don't match, and press Escape to clear the filter. This is especially useful for hiding built-in google protobuf definitions.
|
|
714
|
+
|
|
715
|
+
#### REPL mode
|
|
716
|
+
|
|
717
|
+
Launch with `--repl` / `-r` for step-by-step text navigation with tab completion:
|
|
718
|
+
|
|
719
|
+
```bash
|
|
720
|
+
wizard-codegen context-explorer -r
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
```
|
|
724
|
+
Context Explorer - type help for commands
|
|
725
|
+
|
|
726
|
+
context:/> message
|
|
727
|
+
Key Type Preview
|
|
728
|
+
.service.wizard.pages.sample.v1.SampleProtoPage dict (8 keys) name, full_name, fields, ...
|
|
729
|
+
|
|
730
|
+
context:/message> [.service.wizard.pages.sample.v1.SampleProtoPage]
|
|
731
|
+
Key Type Preview
|
|
732
|
+
name Name SampleProtoPage
|
|
733
|
+
full_name str .service.wizard.pages.sample.v1.SampleProtoPage
|
|
734
|
+
fields list (0 items)
|
|
735
|
+
nested_messages list (2 items)
|
|
736
|
+
samples list (2 items)
|
|
737
|
+
default_sample dict (3 keys) name, context, form
|
|
738
|
+
|
|
739
|
+
context:/message[.service.wizard.pages.sample.v1.SampleProtoPage]> name
|
|
740
|
+
SampleProtoPage (raw=SampleProtoPage, snake=sample_proto_page, kebab=sample-proto-page,
|
|
741
|
+
pascal=SampleProtoPage, camel=sampleProtoPage, macro=SAMPLEPROTOPAGE,
|
|
742
|
+
macro_snake=SAMPLE_PROTO_PAGE, micro=sampleprotopage)
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
REPL commands:
|
|
746
|
+
|
|
747
|
+
| Command | Description |
|
|
748
|
+
|----------------|----------------------------------------------------------------|
|
|
749
|
+
| `<path>` | Navigate to path (dot-separated, `[brackets]` for dict/list) |
|
|
750
|
+
| `keys` | Show available keys at current level |
|
|
751
|
+
| `..` | Go up one level |
|
|
752
|
+
| `/` | Go to root |
|
|
753
|
+
| `tree [depth]` | Show tree from current position (optional depth limit) |
|
|
754
|
+
| `help` | Show help |
|
|
755
|
+
| `quit` | Exit the explorer |
|
|
756
|
+
|
|
757
|
+
Path syntax uses dots for plain keys and brackets for keys containing dots or list indices: `message[.pkg.Foo].fields[0].name.snake_case`.
|
|
758
|
+
|
|
606
759
|
### Name Transformations
|
|
607
760
|
|
|
608
761
|
Every name in the context is wrapped in a `Name` object that provides automatic case transformations:
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from utils.name import Name
|
|
6
|
+
|
|
7
|
+
NAME_PROPERTIES = [
|
|
8
|
+
"raw", "snake_case", "kebab_case", "pascal_case",
|
|
9
|
+
"camel_case", "macro_case", "macro_snake_case", "micro_case",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PathError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_path(path_str: str) -> list[str | int]:
|
|
18
|
+
"""Tokenize a path like ``message[.pkg.Foo].fields[0]`` into segments.
|
|
19
|
+
|
|
20
|
+
Returns a list of string keys and integer indices.
|
|
21
|
+
"""
|
|
22
|
+
segments: list[str | int] = []
|
|
23
|
+
s = path_str.strip()
|
|
24
|
+
if not s:
|
|
25
|
+
return segments
|
|
26
|
+
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(s):
|
|
29
|
+
if s[i] == "[":
|
|
30
|
+
try:
|
|
31
|
+
end = s.index("]", i + 1)
|
|
32
|
+
except ValueError:
|
|
33
|
+
raise PathError(f"unmatched '[' at position {i} in: {path_str}")
|
|
34
|
+
inner = s[i + 1 : end]
|
|
35
|
+
if not inner:
|
|
36
|
+
raise PathError(f"empty brackets at position {i} in: {path_str}")
|
|
37
|
+
try:
|
|
38
|
+
segments.append(int(inner))
|
|
39
|
+
except ValueError:
|
|
40
|
+
segments.append(inner)
|
|
41
|
+
i = end + 1
|
|
42
|
+
if i < len(s) and s[i] == ".":
|
|
43
|
+
i += 1
|
|
44
|
+
elif s[i] == ".":
|
|
45
|
+
i += 1
|
|
46
|
+
else:
|
|
47
|
+
end = i
|
|
48
|
+
while end < len(s) and s[end] not in ".[]":
|
|
49
|
+
end += 1
|
|
50
|
+
segments.append(s[i:end])
|
|
51
|
+
i = end
|
|
52
|
+
if i < len(s) and s[i] == ".":
|
|
53
|
+
i += 1
|
|
54
|
+
|
|
55
|
+
return segments
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _step_into(data: Any, segment: str | int) -> Any:
|
|
59
|
+
if isinstance(segment, int):
|
|
60
|
+
if isinstance(data, list):
|
|
61
|
+
if 0 <= segment < len(data):
|
|
62
|
+
return data[segment]
|
|
63
|
+
raise PathError(f"index {segment} out of range (length {len(data)})")
|
|
64
|
+
raise PathError(f"cannot index into {type(data).__name__}")
|
|
65
|
+
|
|
66
|
+
if isinstance(data, dict):
|
|
67
|
+
if segment in data:
|
|
68
|
+
return data[segment]
|
|
69
|
+
available = ", ".join(list(data.keys())[:10])
|
|
70
|
+
if len(data) > 10:
|
|
71
|
+
available += ", ..."
|
|
72
|
+
raise PathError(f"key '{segment}' not found. Available: {available}")
|
|
73
|
+
|
|
74
|
+
if isinstance(data, Name):
|
|
75
|
+
if segment in NAME_PROPERTIES:
|
|
76
|
+
return getattr(data, segment)
|
|
77
|
+
raise PathError(
|
|
78
|
+
f"Name has no property '{segment}'. "
|
|
79
|
+
f"Available: {', '.join(NAME_PROPERTIES)}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if isinstance(data, list):
|
|
83
|
+
raise PathError(f"use bracket index [N] for lists (length {len(data)})")
|
|
84
|
+
|
|
85
|
+
raise PathError(f"cannot navigate into {type(data).__name__}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def resolve_segments(data: Any, segments: list[str | int]) -> Any:
|
|
89
|
+
"""Navigate into *data* following pre-parsed *segments*."""
|
|
90
|
+
current = data
|
|
91
|
+
for seg in segments:
|
|
92
|
+
current = _step_into(current, seg)
|
|
93
|
+
return current
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_path(data: Any, path_str: str) -> Any:
|
|
97
|
+
"""Navigate into *data* using a dot/bracket path string."""
|
|
98
|
+
return resolve_segments(data, parse_path(path_str))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_keys(data: Any) -> list[str]:
|
|
102
|
+
"""Return navigable keys/indices for *data*.
|
|
103
|
+
|
|
104
|
+
List indices are returned as plain digit strings (``"0"``, ``"1"``).
|
|
105
|
+
Callers that need bracket notation for display should wrap them.
|
|
106
|
+
"""
|
|
107
|
+
if isinstance(data, dict):
|
|
108
|
+
return list(str(k) for k in data.keys())
|
|
109
|
+
if isinstance(data, list):
|
|
110
|
+
return [str(i) for i in range(len(data))]
|
|
111
|
+
if isinstance(data, Name):
|
|
112
|
+
return list(NAME_PROPERTIES)
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def needs_brackets(key: str) -> bool:
|
|
117
|
+
"""True when *key* must be wrapped in ``[...]`` because it contains dots."""
|
|
118
|
+
return "." in key or key.startswith(".")
|