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.
Files changed (31) hide show
  1. wizard_codegen-0.2.1/README.md → wizard_codegen-0.3.0/PKG-INFO +188 -3
  2. wizard_codegen-0.2.1/PKG-INFO → wizard_codegen-0.3.0/README.md +171 -18
  3. wizard_codegen-0.3.0/cli/context_explorer/__init__.py +5 -0
  4. wizard_codegen-0.3.0/cli/context_explorer/path_resolver.py +118 -0
  5. wizard_codegen-0.3.0/cli/context_explorer/repl.py +319 -0
  6. wizard_codegen-0.3.0/cli/context_explorer/tree.py +164 -0
  7. wizard_codegen-0.3.0/cli/context_explorer/tui.py +591 -0
  8. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/cli/main.py +121 -23
  9. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/__init__.py +5 -2
  10. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/filter.py +5 -0
  11. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/renderer.py +9 -2
  12. wizard_codegen-0.3.0/core/samples.py +347 -0
  13. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/writer.py +1 -2
  14. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/pyproject.toml +6 -3
  15. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/.gitignore +0 -0
  16. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/cli/__init__.py +0 -0
  17. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/config.py +0 -0
  18. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/core/context_builder.py +0 -0
  19. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/hooks/__init__.py +0 -0
  20. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/hooks/hooks.py +0 -0
  21. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/__init__.py +0 -0
  22. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/discover.py +0 -0
  23. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/fds_loader.py +0 -0
  24. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/proto_source.py +0 -0
  25. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/proto/protoc_runner.py +0 -0
  26. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/tests/fixtures/hooks/__init__.py +0 -0
  27. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/tests/fixtures/hooks/type_mapping.py +0 -0
  28. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/__init__.py +0 -0
  29. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/name.py +0 -0
  30. {wizard_codegen-0.2.1 → wizard_codegen-0.3.0}/utils/options.py +0 -0
  31. {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
  [![CircleCI](https://dl.circleci.com/status-badge/img/gh/ConsultingMD/wizard-codegen/tree/main.svg?style=svg&circle-token=CCIPRJ_6cYihJ2CPYtLrt8VjrCcSV_f02bbbce30f122a85da4e93588e8887e973128fd)](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 --config custom.yaml generate
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
  [![CircleCI](https://dl.circleci.com/status-badge/img/gh/ConsultingMD/wizard-codegen/tree/main.svg?style=svg&circle-token=CCIPRJ_6cYihJ2CPYtLrt8VjrCcSV_f02bbbce30f122a85da4e93588e8887e973128fd)](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 --config custom.yaml generate
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,5 @@
1
+ from .tree import show_context_tree
2
+ from .repl import run_repl
3
+ from .tui import run_tui
4
+
5
+ __all__ = ["show_context_tree", "run_repl", "run_tui"]
@@ -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(".")