wizard-codegen 0.2.2__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.2 → wizard_codegen-0.3.0}/PKG-INFO +159 -1
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/README.md +156 -0
- 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.2 → wizard_codegen-0.3.0}/cli/main.py +56 -2
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/core/__init__.py +5 -2
- wizard_codegen-0.3.0/core/samples.py +347 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/core/writer.py +1 -2
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/pyproject.toml +4 -1
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/.gitignore +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/cli/__init__.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/core/config.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/core/context_builder.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/core/filter.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/core/renderer.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/hooks/__init__.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/hooks/hooks.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/proto/__init__.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/proto/discover.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/proto/fds_loader.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/proto/proto_source.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/proto/protoc_runner.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/tests/fixtures/hooks/__init__.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/tests/fixtures/hooks/type_mapping.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/utils/__init__.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/utils/name.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/utils/options.py +0 -0
- {wizard_codegen-0.2.2 → wizard_codegen-0.3.0}/utils/path.py +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wizard-codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A powerful, template-driven code generation tool for Protocol Buffers
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: click==8.3.1
|
|
7
7
|
Requires-Dist: jinja2==3.1.6
|
|
8
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
8
9
|
Requires-Dist: protobuf==6.33.5
|
|
9
10
|
Requires-Dist: pydantic-core==2.41.5
|
|
10
11
|
Requires-Dist: pydantic==2.12.5
|
|
11
12
|
Requires-Dist: pyyaml==6.0.3
|
|
12
13
|
Requires-Dist: rich==14.3.3
|
|
14
|
+
Requires-Dist: textual>=8.1.1
|
|
13
15
|
Requires-Dist: typer==0.21.1
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
15
17
|
|
|
@@ -71,6 +73,9 @@ handlers — this tool has you covered.
|
|
|
71
73
|
| **Verbose Output** | Debug with detailed proto and context inspection |
|
|
72
74
|
| **Nested Type Support** | Full support for nested messages and enums |
|
|
73
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) |
|
|
74
79
|
|
|
75
80
|
---
|
|
76
81
|
|
|
@@ -513,6 +518,8 @@ When a template is rendered, it receives a rich context:
|
|
|
513
518
|
"fields": [...], # List of field objects
|
|
514
519
|
"nested_messages": [...], # Nested message definitions
|
|
515
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)
|
|
516
523
|
},
|
|
517
524
|
|
|
518
525
|
# All files (for cross-referencing)
|
|
@@ -615,6 +622,157 @@ When a template is rendered, it receives a rich context:
|
|
|
615
622
|
}
|
|
616
623
|
```
|
|
617
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
|
+
|
|
618
776
|
### Name Transformations
|
|
619
777
|
|
|
620
778
|
Every name in the context is wrapped in a `Name` object that provides automatic case transformations:
|
|
@@ -56,6 +56,9 @@ handlers — this tool has you covered.
|
|
|
56
56
|
| **Verbose Output** | Debug with detailed proto and context inspection |
|
|
57
57
|
| **Nested Type Support** | Full support for nested messages and enums |
|
|
58
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) |
|
|
59
62
|
|
|
60
63
|
---
|
|
61
64
|
|
|
@@ -498,6 +501,8 @@ When a template is rendered, it receives a rich context:
|
|
|
498
501
|
"fields": [...], # List of field objects
|
|
499
502
|
"nested_messages": [...], # Nested message definitions
|
|
500
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)
|
|
501
506
|
},
|
|
502
507
|
|
|
503
508
|
# All files (for cross-referencing)
|
|
@@ -600,6 +605,157 @@ When a template is rendered, it receives a rich context:
|
|
|
600
605
|
}
|
|
601
606
|
```
|
|
602
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
|
+
|
|
603
759
|
### Name Transformations
|
|
604
760
|
|
|
605
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(".")
|