ctx_parser 0.1.1 → 0.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README-zh.md +14 -4
- data/README.md +16 -6
- data/docs/architecture-zh.md +45 -33
- data/lib/ctx_parser/block.rb +14 -2
- data/lib/ctx_parser/{command_catalog.rb → commands.rb} +9 -8
- data/lib/ctx_parser/{configuration_parser.rb → configuration.rb} +19 -3
- data/lib/ctx_parser/document.rb +1 -1
- data/lib/ctx_parser/immutable.rb +70 -5
- data/lib/ctx_parser/input.rb +8 -5
- data/lib/ctx_parser/parser.rb +4 -15
- data/lib/ctx_parser/profile.rb +19 -18
- data/lib/ctx_parser/records.rb +9 -9
- data/lib/ctx_parser/registry.rb +4 -0
- data/lib/ctx_parser/result.rb +2 -0
- data/lib/ctx_parser/results.rb +43 -0
- data/lib/ctx_parser/values.rb +18 -10
- data/lib/ctx_parser/version.rb +1 -1
- data/lib/ctx_parser.rb +2 -2
- metadata +5 -5
- data/lib/ctx_parser/result_set.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34175c1684f6dc5b5a7ef9a12649f16d393965f87c506d1f672f63a8301a7ebd
|
|
4
|
+
data.tar.gz: 55f14e3f497e6a3fa5484f6e5465effc39acadcfff0e908932abe41b43696b94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc1d527a3d2a6a0c2001bfc270d6afb7f8b21e3f779d5f61b31de69757ea598f2b255a5a5eb15db41ebde8de6274c99e3ade55f89cb1b577041cdab8d4563bbf
|
|
7
|
+
data.tar.gz: 4708ac383d49d5f2ade14747800d6d5d52db0e1cdc78ac2acbd87e828fb41695e41ebf2a98a72b1c81c13d4612c2419e40ba189d429c39be7bb4afebd074100c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.2.1 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- Update the configuration benchmark to use the Baseline 0.2 vendor parser namespace.
|
|
8
|
+
|
|
9
|
+
## 0.2.0 (2026-10-17)
|
|
10
|
+
|
|
11
|
+
- Validate integer inputs and bound types without implicit object conversion; reject mixed MAC separators.
|
|
12
|
+
- Strip hash defaults from result dimension maps and reject non-hash registry declarations at the public boundary.
|
|
13
|
+
- Append owned command indexes in place during construction and avoid reselecting an already validated command during profile parsing.
|
|
14
|
+
|
|
15
|
+
- Keep result interpolation redacted and reject malformed diagnostic collections.
|
|
16
|
+
- Detach nested document diagnostics and reject custom mutable numeric values in fact snapshots, including Complex components.
|
|
17
|
+
- Validate child block ownership, containment, and source order before accepting a source tree.
|
|
18
|
+
- Short-circuit block completeness queries without allocating line and state arrays; retain live cursor semantics.
|
|
19
|
+
|
|
20
|
+
- Reject unnamed or non-symbol result dimensions before profile selection.
|
|
21
|
+
|
|
22
|
+
- Use `Configuration`, `Results`, and `Commands` consistently; keep configuration source state in `Configuration` and name dimension indexes `by_dimension`.
|
|
23
|
+
|
|
24
|
+
- Require a boolean `allow_zone` option so string values cannot enable scoped IPv6 addresses.
|
|
25
|
+
|
|
26
|
+
- Reject nontext address identities and nonboolean host-bit preservation policies at the scalar boundary.
|
|
27
|
+
|
|
28
|
+
- Rename domain APIs directly; callers must use the new names (no compatibility aliases).
|
|
29
|
+
|
|
30
|
+
- Clarify dimension-to-command planning, model parser overrides, and record identity grouping names.
|
|
31
|
+
|
|
32
|
+
- Copy shared fact containers once, reject cyclic values with `ArgumentError`, and support deeply nested snapshots without exhausting the Ruby call stack.
|
|
33
|
+
- Preserve valid UTF-16/32 source encodings while checking logical NUL characters and normalizing requested line endings.
|
|
34
|
+
|
|
5
35
|
## 0.1.1 (2026-10-16)
|
|
6
36
|
|
|
7
37
|
- Publish verified artifacts using either an environment API key or RubyGems Trusted Publishing.
|
data/README-zh.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## 能力
|
|
8
8
|
|
|
9
|
-
- `Parser`、`
|
|
9
|
+
- `Parser`、`Configuration` 定义文本输入及完整性边界。
|
|
10
10
|
- `Cursor`、`Document`、`Capture` 保留来源位置和嵌套配置上下文。
|
|
11
|
-
- `Profile`、`
|
|
11
|
+
- `Profile`、`Commands`、`Registry` 声明平台/型号行为,同一回显可共享解析。
|
|
12
12
|
- `Values`、`Records`、`Immutable` 统一值转换、身份检查与防御性复制。
|
|
13
13
|
|
|
14
14
|
## 安装
|
|
@@ -68,16 +68,26 @@ result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
|
68
68
|
|
|
69
69
|
## 公开契约与限制
|
|
70
70
|
|
|
71
|
-
`Parser.parse(text, complete: false)` 返回包含 `status`、`records`、`issues` 的 `Result`;多视图解析器可返回 `
|
|
71
|
+
`Parser.parse(text, complete: false)` 返回包含 `status`、`records`、`issues` 的 `Result`;多视图解析器可返回 `Results`。状态为 `complete`、`partial`、`failed`、`unsupported`。子类实现 `parse_output`,配置子类通过 `analyze` 和 `capture` 保留来源。
|
|
72
72
|
|
|
73
|
-
`complete: true`
|
|
73
|
+
`complete: true` 只表示调用方确认输入完整接收,不证明所有语法都已理解。未知行、身份冲突及空表证据不足必须明确表达。结果的记录和诊断均须为 Hash 数组;结果及构建完成的文档诊断持有不可变快照。`inspect` 和 `to_s` 不输出结果正文与诊断内容,显式读取记录仍返回原始事实。来源游标有意保留输入原文,含秘密时不可写入日志。
|
|
74
74
|
|
|
75
75
|
本 gem 不提供厂商命令目录、传输、持久化或健康策略。参数和注册表配置错误保持异常,由调用方在对应边界处理。
|
|
76
76
|
|
|
77
77
|
详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
|
|
78
78
|
|
|
79
|
+
## 领域术语
|
|
80
|
+
|
|
81
|
+
名称使用领域名词:`Configuration` 解析配置文档,`Result` 表示单个结果,`Results` 保存具名结果,`Commands` 保存命令声明。两个集合均用 `by_dimension` 表示维度索引。通用 `Parser` 负责文本解析契约,配置来源状态归 `Configuration` 管理。
|
|
82
|
+
|
|
83
|
+
维度是具名事实视图,命令是提供这些事实的 CLI 查询。`Profile#queries` 按命令归组维度,型号覆盖按命令选择解析器。`Records` 按逻辑身份归组记录;重复身份数与多余记录数是不同数量。`Line`、`Block`、`Capture` 保留来源位置,`Result` 承载归一化事实与诊断。解析完整性或空结果都不能证明设备健康。
|
|
84
|
+
|
|
85
|
+
地址转换使用 `Values.ip_address`、`Values.ip_address?`、`Values.mac_address`。`Values.ipv4_prefix(..., preserve_host_bits: true)` 明确保留接口主机位,默认返回网段前缀。
|
|
86
|
+
|
|
79
87
|
## 开发与测试
|
|
80
88
|
|
|
89
|
+
共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
|
|
90
|
+
|
|
81
91
|
```sh
|
|
82
92
|
bundle install
|
|
83
93
|
bundle exec rake
|
data/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](README-zh.md) | [Source](https://github.com/gatework/ctx_parser) | [Changelog](CHANGELOG.md)
|
|
4
4
|
|
|
5
|
-
Shared, pure Ruby building blocks for network CLI parsing. It provides immutable results, source cursors, configuration trees, command
|
|
5
|
+
Shared, pure Ruby building blocks for network CLI parsing. It provides immutable results, source cursors, configuration trees, command declarations, and platform profiles without owning vendor syntax or device sessions.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- `Parser` and `
|
|
9
|
+
- `Parser` and `Configuration` define explicit text and completeness boundaries.
|
|
10
10
|
- `Cursor`, `Document`, and `Capture` preserve source positions and nested configuration context.
|
|
11
|
-
- `Profile`, `
|
|
11
|
+
- `Profile`, `Commands`, and `Registry` select declared platform/model behavior and share parsing across views.
|
|
12
12
|
- `Values`, `Records`, and `Immutable` centralize conversion, identity checks, and defensive copies.
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
@@ -68,16 +68,26 @@ result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
|
68
68
|
|
|
69
69
|
## Public contract and limits
|
|
70
70
|
|
|
71
|
-
`Parser.parse(text, complete: false)` returns a `Result` with `status`, `records`, and `issues`; a multi-view parser can return `
|
|
71
|
+
`Parser.parse(text, complete: false)` returns a `Result` with `status`, `records`, and `issues`; a multi-view parser can return `Results`. Result statuses are `complete`, `partial`, `failed`, and `unsupported`. Subclasses implement `parse_output`. Configuration subclasses can use `analyze` and `capture` to preserve provenance.
|
|
72
72
|
|
|
73
|
-
`complete: true` confirms that the caller received the entire input; it does not certify that all syntax was understood. Unknown rows, identity conflicts, and insufficient empty-table evidence must remain explicit. Results
|
|
73
|
+
`complete: true` confirms that the caller received the entire input; it does not certify that all syntax was understood. Unknown rows, identity conflicts, and insufficient empty-table evidence must remain explicit. Result records and issues must be arrays of hashes. Results and completed document diagnostics own immutable snapshots. `inspect` and `to_s` omit result facts and diagnostics; explicit record access still returns the original facts. Source cursors intentionally retain original input and must not be logged when it contains secrets.
|
|
74
74
|
|
|
75
|
-
This gem supplies no vendor command
|
|
75
|
+
This gem supplies no vendor command declarations, transport, persistence, or health policy. Parser/registry argument errors remain exceptions for callers to handle at their own boundary.
|
|
76
76
|
|
|
77
77
|
Detailed vendor behavior, historical validation notes, and Motor-only integration are preserved in [the architecture reference](docs/architecture-zh.md). Host integration examples in that reference require Motor; they are not standalone gem APIs. Motor adapters live under `backend/lib/baseline`, `backend/lib/vxlan`, and `backend/lib/wireless` in the [Motor repository](https://github.com/gatework/motor/tree/main/backend/lib).
|
|
78
78
|
|
|
79
|
+
## Domain vocabulary
|
|
80
|
+
|
|
81
|
+
Names use domain nouns: `Configuration` parses configuration documents; `Result` holds one result and `Results` holds named results; `Commands` stores command declarations. Both collections expose their dimension index as `by_dimension`. Generic `Parser` owns the text parsing contract; configuration source state belongs to `Configuration`.
|
|
82
|
+
|
|
83
|
+
A dimension is a named view of facts; a command is the CLI query that supplies those facts. `Profile#queries` groups dimensions by command, and model overrides choose parsers for commands. `Records` groups rows by logical identity; duplicate identity counts and redundant row counts are different quantities. `Line`, `Block`, and `Capture` retain source locations, while `Result` contains normalized facts and diagnostics. Neither parsing completeness nor an empty result establishes device health.
|
|
84
|
+
|
|
85
|
+
Address conversion uses `Values.ip_address`, `Values.ip_address?`, and `Values.mac_address`. `Values.ipv4_prefix(..., preserve_host_bits: true)` explicitly preserves interface host bits; its default returns a network prefix.
|
|
86
|
+
|
|
79
87
|
## Development and tests
|
|
80
88
|
|
|
89
|
+
Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
|
|
90
|
+
|
|
81
91
|
```sh
|
|
82
92
|
bundle install
|
|
83
93
|
bundle exec rake
|
data/docs/architecture-zh.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| 对象 | 职责 |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| `Parser` | 统一 `.parse(text, complete: false)` 接口和参数校验;具体策略实现 `parse_output` |
|
|
14
|
-
| `
|
|
14
|
+
| `Configuration` | 持有配置文档、游标和节点,管理来源状态、捕获与嵌套路径;配置语法由具体策略处理 |
|
|
15
15
|
| `Cursor` | 保存原文,顺序读取、窥视、回退,独立记录访问状态和逐行解析状态 |
|
|
16
16
|
| `Line` | 不可变的源行,保留原始文本;构造时计算并缓存缩进和匹配文本 |
|
|
17
17
|
| `Block` | 配置树节点,保存父子层次、源行范围和完整配置块原文 |
|
|
@@ -22,15 +22,21 @@
|
|
|
22
22
|
| `Records` | 按调用方的身份键分析重复项,提供首条保留与无歧义投影 |
|
|
23
23
|
| `Immutable` | 复制并冻结纯数据,与调用方的可变对象隔离 |
|
|
24
24
|
| `Result` | 单个清单的 `status / records / issues` |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
25
|
+
| `Results` | 以 `by_dimension` 保存同一回显的具名结果,各维度独立保留 `Result` 和完整性 |
|
|
26
|
+
| `Commands` | 一次声明命令、解析器与结果维度,校验并生成不可变索引 |
|
|
27
27
|
| `Profile` | 组合命令与解析器,按型号覆盖策略,共享命令只解析一次 |
|
|
28
28
|
| `Registry` | 由业务模块声明支持的平台和别名,选择对应 `Profile` |
|
|
29
29
|
|
|
30
|
-
公共库不导入具体厂商,也不维护任何命令正则、设备会话或数据库投影。业务模块按职责复用公共抽象;VXLAN、Wireless 使用命令注册表,Baseline 直接从配置文本进入 `
|
|
30
|
+
公共库不导入具体厂商,也不维护任何命令正则、设备会话或数据库投影。业务模块按职责复用公共抽象;VXLAN、Wireless 使用命令注册表,Baseline 直接从配置文本进入 `Configuration`。
|
|
31
31
|
|
|
32
|
-
`Profile#queries(names)`
|
|
33
|
-
|
|
32
|
+
`Profile#queries(names)` 生成冻结的命令到维度计划,保留选择顺序并合并共享命令,支持同一维度的多条脚本。`parse_many` 选择覆盖全部维度的命令后直接查找解析器,型号覆盖仍优先;不重复扫描同一维度的命令列表,也不缓存可能被动态扩展替换的命令表。
|
|
33
|
+
|
|
34
|
+
`Results#by_dimension` 是不带默认值或默认回调的普通冻结 Hash。未声明维度的下标读取返回 nil,`fetch` 抛出 KeyError,不能由调用方 Hash 的默认规则伪造解析证据。
|
|
35
|
+
Result 的记录和诊断必须为 Hash 数组,是防御性复制后的不可变数据;通过 `with(...)` 替换,不能原地修改。文档在词法分析完成后也复制并深度冻结诊断,避免分类器保留的引用修改已有证据。
|
|
36
|
+
|
|
37
|
+
`Immutable` 接受普通 Hash、Array、String、Symbol、nil、布尔值及内建 Integer、Float、Rational、Complex。Complex 的分量须满足同一规则;不接受自定义 Numeric 对象,即使其外层已冻结。Float 的非有限值仍保留,是否允许由具体字段语义决定。
|
|
38
|
+
|
|
39
|
+
配置块的子节点必须属于同一游标、位于父范围内并按来源顺序排列,不可重复或重叠。完整性查询直接读取当前行状态,遇到未解决行立即返回;不会缓存可能过时的完整性结论。`benchmark/completeness.rb` 独立测量该查询,不代表端到端设备解析性能。
|
|
34
40
|
具体兼容边界见 [模块架构](https://github.com/gatework/motor/blob/main/backend/vendor/ARCHITECTURE.md)。
|
|
35
41
|
|
|
36
42
|
```text
|
|
@@ -42,14 +48,14 @@ vendor/
|
|
|
42
48
|
block.rb
|
|
43
49
|
capture.rb
|
|
44
50
|
document.rb
|
|
45
|
-
|
|
51
|
+
configuration.rb
|
|
46
52
|
input.rb
|
|
47
53
|
values.rb
|
|
48
54
|
records.rb
|
|
49
55
|
immutable.rb
|
|
50
56
|
result.rb
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
results.rb
|
|
58
|
+
commands.rb
|
|
53
59
|
profile.rb
|
|
54
60
|
registry.rb
|
|
55
61
|
vxlan/lib/vxlan/
|
|
@@ -58,6 +64,7 @@ vendor/
|
|
|
58
64
|
profile.rb
|
|
59
65
|
parser.rb
|
|
60
66
|
configuration.rb # 特性、模板与网段事实归约
|
|
67
|
+
document.rb # 配置来源文档
|
|
61
68
|
evpn_l2route.rb
|
|
62
69
|
parsers/show_*.rb # 12 条 NX-OS 命令
|
|
63
70
|
wireless/lib/wireless/
|
|
@@ -68,10 +75,10 @@ vendor/
|
|
|
68
75
|
parser.rb
|
|
69
76
|
command_output.rb # 配置与运行命令共用输入边界
|
|
70
77
|
configuration.rb # 实体身份、归属与认证类型
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
parsers/display_*.rb #
|
|
78
|
+
document.rb # 配置来源文档
|
|
79
|
+
record_parser.rb # 标签字段与记录分组
|
|
80
|
+
authentication.rb # 认证字段与身份定义
|
|
81
|
+
parsers/display_*.rb # 10 条 H3C 命令
|
|
75
82
|
baseline/lib/baseline/
|
|
76
83
|
document.rb # Baseline 自身的 banner 和提示行分类
|
|
77
84
|
facts.rb # 跨命令的事实归约,不依赖解析游标
|
|
@@ -83,17 +90,18 @@ classDiagram
|
|
|
83
90
|
CtxParser <|-- NxosParser
|
|
84
91
|
CtxParser <|-- H3cParser
|
|
85
92
|
NxosParser <|-- ShowIpOspfNeighbors
|
|
86
|
-
H3cParser <|--
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
H3cParser <|-- RecordParser
|
|
94
|
+
RecordParser <|-- DisplayWlanApAllVerbose
|
|
95
|
+
RecordParser <|-- DisplayWlanClientVerbose
|
|
96
|
+
RecordParser <|-- DisplayDot1xConnection
|
|
97
|
+
Authentication <.. DisplayDot1xConnection : includes field definitions
|
|
90
98
|
CtxParserProfile <|-- NxosProfile
|
|
91
99
|
CtxParserProfile <|-- H3cProfile
|
|
92
100
|
Registry --> CtxParserProfile : selects
|
|
93
101
|
CtxParserProfile --> CtxParser : composes
|
|
94
102
|
CtxParser --> Result : returns
|
|
95
|
-
CtxParser <|--
|
|
96
|
-
|
|
103
|
+
CtxParser <|-- Configuration
|
|
104
|
+
Configuration --> Cursor : owns per parse
|
|
97
105
|
Cursor --> Line : reads and tracks
|
|
98
106
|
Cursor --> Capture : records
|
|
99
107
|
Capture --> Block : source
|
|
@@ -101,13 +109,13 @@ classDiagram
|
|
|
101
109
|
Document --> Cursor : owns
|
|
102
110
|
Document --> Block : builds
|
|
103
111
|
BaselineDocument --|> Document : line classification
|
|
104
|
-
BaselineParser --|>
|
|
112
|
+
BaselineParser --|> Configuration : analyze and capture
|
|
105
113
|
BaselineParser --> BaselineFacts : normalized observations
|
|
106
|
-
ShowRunningConfig --|>
|
|
107
|
-
DisplayCurrentConfiguration --|>
|
|
108
|
-
|
|
109
|
-
DisplayWlanApAllVerbose -->
|
|
110
|
-
|
|
114
|
+
ShowRunningConfig --|> Configuration
|
|
115
|
+
DisplayCurrentConfiguration --|> Configuration
|
|
116
|
+
Configuration --> Capture : records scopes
|
|
117
|
+
DisplayWlanApAllVerbose --> Results : returns AP and radio results
|
|
118
|
+
Results --> Result : contains
|
|
111
119
|
```
|
|
112
120
|
|
|
113
121
|
## 调用
|
|
@@ -137,7 +145,7 @@ results = h3c.parse_many(%i[access_points radios], ap_output, complete: true)
|
|
|
137
145
|
results.fetch(:access_points).records
|
|
138
146
|
results.fetch(:radios).records
|
|
139
147
|
|
|
140
|
-
# 直接调用共享命令时,返回
|
|
148
|
+
# 直接调用共享命令时,返回 Results。
|
|
141
149
|
parsed = Wireless::H3c::Parsers::DisplayWlanApAllVerbose.parse(ap_output, complete: true)
|
|
142
150
|
parsed.fetch(:access_points).complete?
|
|
143
151
|
parsed.fetch(:radios).issues
|
|
@@ -215,7 +223,7 @@ VXLAN、Wireless 和 Baseline 使用同一套输入与结构能力;各模块
|
|
|
215
223
|
```ruby
|
|
216
224
|
CtxParser::Input.text(raw, encoding: :utf8, newlines: :lf)
|
|
217
225
|
CtxParser::Values.integer("4094", min: 1, max: 4094)
|
|
218
|
-
CtxParser::Values.
|
|
226
|
+
CtxParser::Values.mac_address("aabb.ccdd.eeff") # => "aa:bb:cc:dd:ee:ff"
|
|
219
227
|
|
|
220
228
|
rows = CtxParser::Records.new(records, identity: [ :name ])
|
|
221
229
|
rows.first_occurrences
|
|
@@ -229,24 +237,28 @@ snapshot = CtxParser::Immutable.copy({ "facts" => [ "ssh" ] })
|
|
|
229
237
|
|
|
230
238
|
## 新厂商、型号和格式
|
|
231
239
|
|
|
240
|
+
`Values.integer` 只接受非负 Integer、完整十进制文本或表示缺失的 nil。`min` / `max` 仅接受 Integer 或 nil,且上限不得小于下限;即使原始值缺失,也校验选项。MAC 的四位分组格式必须一致使用点号或连字符,不混用分隔符。
|
|
241
|
+
|
|
232
242
|
新增厂商时,在所属业务模块下建立平台目录、具体命令类和 `Profile`,加入该模块的 `ParserRegistry`。无需修改公共库或其他厂商的解析代码。
|
|
233
243
|
|
|
234
|
-
每个平台通过普通 Ruby 数据声明一份 `
|
|
244
|
+
每个平台通过普通 Ruby 数据声明一份 `DECLARATIONS`,命令字符串只出现一次。相同回显的多个视图放在同一条声明中:
|
|
235
245
|
|
|
236
246
|
```ruby
|
|
237
|
-
|
|
247
|
+
DECLARATIONS = CtxParser::Commands.new([
|
|
238
248
|
{ dimensions: %i[access_points radios], command: "display wlan ap all verbose",
|
|
239
249
|
parser: Parsers::DisplayWlanApAllVerbose },
|
|
240
250
|
{ dimensions: [ :clients ], command: "display wlan client verbose",
|
|
241
251
|
parser: Parsers::DisplayWlanClientVerbose }
|
|
242
252
|
])
|
|
243
|
-
COMMANDS =
|
|
244
|
-
PARSERS =
|
|
253
|
+
COMMANDS = DECLARATIONS.by_dimension
|
|
254
|
+
PARSERS = DECLARATIONS.parsers
|
|
245
255
|
```
|
|
246
256
|
|
|
247
|
-
`
|
|
257
|
+
`Registry` 的平台和别名声明必须为 Hash,归一化名称冲突、不存在的别名目标及别名链均拒绝,配置错误统一在构造边界抛出 ArgumentError。
|
|
258
|
+
|
|
259
|
+
`Commands` 在构造时拒绝空声明、重复命令或维度、含控制字符的命令,以及非 `Parser` 子类。声明数据由本实例复制并持有,命令索引在构建时追加、完成后冻结,避免每次追加复制已有列表;不冻结解析器类或调用方的原始数组。`benchmark/commands.rb` 测量构建和派发成本,保留命令顺序及结果校验。由此派生的 `COMMANDS`(维度到命令)和 `PARSERS`(命令到策略)保持原有查询接口;已有自定义 Profile 仍可使用这两个映射。
|
|
248
260
|
|
|
249
|
-
具有共同语法的命令可复用平台基类;特有字段、表头、身份与总数规则留在对应命令中。会话、命令执行和证据持久化由采集器负责;`
|
|
261
|
+
具有共同语法的命令可复用平台基类;特有字段、表头、身份与总数规则留在对应命令中。会话、命令执行和证据持久化由采集器负责;`Commands` 不执行 I/O,也不引入动态类名查找或自动注册。
|
|
250
262
|
|
|
251
263
|
型号差异用平台 `Profile::MODEL_PARSERS` 声明,形状为 `{ "规范化型号" => { "命令文本" => 专用解析器类 } }`。型号键使用去除首尾空格的小写完整型号,按精确名称匹配;专用解析器可继承已有命令类,仅覆盖有差异的方法。Profile 初始化时校验并复制所选型号的覆盖表:只允许覆盖已声明命令,策略必须继承 `Parser`;修改原表不会改变已创建的 Profile。未覆盖命令和无专用策略的型号使用该平台的公共格式,未知语法仍会产生不完整结果。采集器传入现有 `Motor::Device#model`,AP LLDP 使用 AP 自身型号。
|
|
252
264
|
|
data/lib/ctx_parser/block.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cursor"
|
|
4
|
+
|
|
3
5
|
module CtxParser
|
|
4
6
|
# 配置节点覆盖整个子树,保留未知命令、空白及原始换行。
|
|
5
7
|
class Block
|
|
@@ -10,11 +12,21 @@ module CtxParser
|
|
|
10
12
|
|
|
11
13
|
# 冻结已校验的来源范围及其子配置块。
|
|
12
14
|
def initialize(cursor:, header:, children:, last_index:)
|
|
13
|
-
unless cursor.lines[header.index].equal?(header) && last_index.is_a?(Integer) &&
|
|
15
|
+
unless cursor.is_a?(Cursor) && header.is_a?(Line) && cursor.lines[header.index].equal?(header) && last_index.is_a?(Integer) &&
|
|
14
16
|
(header.index...cursor.size).cover?(last_index)
|
|
15
17
|
raise ArgumentError, "Invalid source block range"
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
previous_index = header.index
|
|
21
|
+
unless children.is_a?(Array) && children.all? do |child|
|
|
22
|
+
next false unless child.is_a?(Block) && child.cursor.equal?(cursor) &&
|
|
23
|
+
child.index > previous_index && child.last_index <= last_index && child.header.indent > header.indent
|
|
24
|
+
|
|
25
|
+
previous_index = child.last_index
|
|
26
|
+
end
|
|
27
|
+
raise ArgumentError, "Child blocks must be ordered within their parent source"
|
|
28
|
+
end
|
|
29
|
+
|
|
18
30
|
@cursor, @header, @last_index = cursor, header, last_index
|
|
19
31
|
@children = children.dup.freeze
|
|
20
32
|
freeze
|
|
@@ -89,7 +101,7 @@ module CtxParser
|
|
|
89
101
|
# 返回当前配置块各来源行的处理状态。
|
|
90
102
|
def states = lines.map { |source| cursor.state(source.index) }.freeze
|
|
91
103
|
# 要求块内所有来源行均已解析或明确忽略。
|
|
92
|
-
def complete? =
|
|
104
|
+
def complete? = (index..last_index).all? { |index| Cursor::RESOLVED_STATES.include?(cursor.state(index)) }
|
|
93
105
|
# 序列化诊断元数据,不包含原始来源文本。
|
|
94
106
|
def as_json(*) = { first_line: line, last_line: last_index + 1, children: children.size }
|
|
95
107
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
@@ -4,21 +4,21 @@ require_relative "parser"
|
|
|
4
4
|
|
|
5
5
|
module CtxParser
|
|
6
6
|
# 每条命令仅声明一次,并派生不可变维度及解析器索引。
|
|
7
|
-
class
|
|
8
|
-
attr_reader :entries, :
|
|
7
|
+
class Commands
|
|
8
|
+
attr_reader :entries, :by_dimension, :parsers
|
|
9
9
|
|
|
10
10
|
# 校验并冻结受支持的维度到命令声明。
|
|
11
11
|
def initialize(entries, multi_command: false)
|
|
12
12
|
unless entries.is_a?(Array) && entries.any?
|
|
13
|
-
raise ArgumentError, "
|
|
13
|
+
raise ArgumentError, "Commands must contain declarations"
|
|
14
14
|
end
|
|
15
15
|
raise ArgumentError, "Multi-command selection must be boolean" unless multi_command == true || multi_command == false
|
|
16
16
|
|
|
17
17
|
@multi_command = multi_command
|
|
18
|
-
@
|
|
18
|
+
@by_dimension, @parsers = {}, {}
|
|
19
19
|
@entries = entries.map { |entry| register(entry) }.freeze
|
|
20
|
-
@
|
|
21
|
-
@
|
|
20
|
+
@by_dimension.each_value { |value| value.freeze }
|
|
21
|
+
@by_dimension.freeze
|
|
22
22
|
@parsers.freeze
|
|
23
23
|
freeze
|
|
24
24
|
end
|
|
@@ -33,7 +33,7 @@ module CtxParser
|
|
|
33
33
|
|
|
34
34
|
dimensions, command, parser = entry.values_at(:dimensions, :command, :parser)
|
|
35
35
|
unless dimensions.is_a?(Array) && dimensions.any? && dimensions.all? { |name| name.is_a?(Symbol) && !name.empty? } &&
|
|
36
|
-
dimensions.uniq == dimensions && (@multi_command || dimensions.none? { |name| @
|
|
36
|
+
dimensions.uniq == dimensions && (@multi_command || dimensions.none? { |name| @by_dimension.key?(name) })
|
|
37
37
|
raise ArgumentError, "Command dimensions must be unique nonempty symbols"
|
|
38
38
|
end
|
|
39
39
|
unless command.is_a?(String) && !command.strip.empty? && !/[\x00-\x1f\x7f]/.match?(command)
|
|
@@ -45,7 +45,8 @@ module CtxParser
|
|
|
45
45
|
command = command.dup.freeze
|
|
46
46
|
dimensions = dimensions.dup.freeze
|
|
47
47
|
dimensions.each do |name|
|
|
48
|
-
|
|
48
|
+
# 索引数组由本实例持有,构建完成后统一冻结;追加不复制已有命令。
|
|
49
|
+
@by_dimension[name] = @by_dimension.key?(name) ? (Array(@by_dimension[name]) << command) : command
|
|
49
50
|
end
|
|
50
51
|
@parsers[command] = parser
|
|
51
52
|
{ dimensions: dimensions, command: command, parser: parser }.freeze
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "parser"
|
|
4
|
+
require_relative "document"
|
|
4
5
|
|
|
5
6
|
module CtxParser
|
|
6
7
|
# 共享来源状态与作用域管理;具体解析器负责识别规则及诊断。
|
|
7
|
-
class
|
|
8
|
+
class Configuration < Parser
|
|
9
|
+
attr_reader :cursor, :blocks, :document
|
|
10
|
+
|
|
11
|
+
# 新解析开始即清空来源,参数校验失败也不暴露上一次配置。
|
|
12
|
+
def parse(text, complete: false)
|
|
13
|
+
@document, @cursor, @blocks = nil, nil, [].freeze
|
|
14
|
+
@source_path = []
|
|
15
|
+
@invalid_source = @source_error = nil
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 只展示配置来源元数据,不序列化原文。
|
|
20
|
+
def as_json(*) = super.merge(cursor: cursor&.as_json, blocks: Array(blocks).size)
|
|
21
|
+
|
|
8
22
|
private
|
|
9
23
|
|
|
10
24
|
# 为新的配置解析建立来源跟踪状态。
|
|
11
|
-
def read_configuration(
|
|
25
|
+
def read_configuration(text, comments: nil, boundaries: nil, document_class: Document)
|
|
12
26
|
@source_path = []
|
|
13
27
|
@invalid_source = @source_error = nil
|
|
14
|
-
|
|
28
|
+
@document = document_class.new(text, comments: comments, boundaries: boundaries)
|
|
29
|
+
@cursor, @blocks = document.cursor, document.roots
|
|
30
|
+
@blocks
|
|
15
31
|
end
|
|
16
32
|
|
|
17
33
|
# 仅在解释成功后标记来源行;嵌套转换失败归因到最内层来源。
|
data/lib/ctx_parser/document.rb
CHANGED
data/lib/ctx_parser/immutable.rb
CHANGED
|
@@ -3,16 +3,81 @@
|
|
|
3
3
|
module CtxParser
|
|
4
4
|
# 复制并冻结普通数据,避免解析快照共享调用方可变状态。
|
|
5
5
|
module Immutable
|
|
6
|
-
|
|
6
|
+
COPYING = Object.new.freeze
|
|
7
|
+
private_constant :COPYING
|
|
8
|
+
|
|
9
|
+
# 同一容器只复制一次;循环数据不是普通值,须在越过快照边界前拒绝。
|
|
7
10
|
def self.copy(value)
|
|
8
|
-
|
|
11
|
+
copy_value(value, {}.compare_by_identity, 0)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 普通常量浅树沿用 Ruby 的映射;递归深度有界,深链交给显式后序栈。
|
|
15
|
+
def self.copy_value(value, copies, depth)
|
|
9
16
|
case value
|
|
10
|
-
when Hash
|
|
11
|
-
|
|
17
|
+
when Hash, Array
|
|
18
|
+
if (snapshot = copies[value])
|
|
19
|
+
raise ArgumentError, "Values must not contain cyclic data" if snapshot.equal?(COPYING)
|
|
20
|
+
|
|
21
|
+
return snapshot
|
|
22
|
+
end
|
|
23
|
+
return copy_deep(value, copies) if depth == 64
|
|
24
|
+
|
|
25
|
+
copies[value] = COPYING
|
|
26
|
+
copies[value] = if value.is_a?(Hash)
|
|
27
|
+
value.to_h { |key, item| [ copy_value(key, copies, depth + 1), copy_value(item, copies, depth + 1) ] }.freeze
|
|
28
|
+
else
|
|
29
|
+
value.map { |item| copy_value(item, copies, depth + 1) }.freeze
|
|
30
|
+
end
|
|
12
31
|
when String then value.frozen? ? value : value.dup.freeze
|
|
13
|
-
when Symbol,
|
|
32
|
+
when Symbol, Integer, Float, Rational, NilClass, TrueClass, FalseClass then value
|
|
33
|
+
when Complex
|
|
34
|
+
# Complex 自身冻结,但可包含调用方定义的可变 Numeric 分量。
|
|
35
|
+
copy_value(value.real, copies, depth + 1)
|
|
36
|
+
copy_value(value.imaginary, copies, depth + 1)
|
|
37
|
+
value
|
|
14
38
|
else raise ArgumentError, "Values must contain plain data"
|
|
15
39
|
end
|
|
16
40
|
end
|
|
41
|
+
private_class_method :copy_value
|
|
42
|
+
|
|
43
|
+
# 容器缓存按来源身份查找,普通 Hash 的键仍沿用值相等语义。
|
|
44
|
+
def self.copy_deep(value, copies)
|
|
45
|
+
pending = [ [ value, false ] ]
|
|
46
|
+
until pending.empty?
|
|
47
|
+
source, finishing = pending.pop
|
|
48
|
+
if finishing
|
|
49
|
+
copies[source] = if source.is_a?(Hash)
|
|
50
|
+
source.to_h { |key, item| [ snapshot(key, copies), snapshot(item, copies) ] }.freeze
|
|
51
|
+
else
|
|
52
|
+
source.map { |item| snapshot(item, copies) }.freeze
|
|
53
|
+
end
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
if (existing = copies[source])
|
|
57
|
+
raise ArgumentError, "Values must not contain cyclic data" if existing.equal?(COPYING)
|
|
58
|
+
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
copies[source] = COPYING
|
|
63
|
+
pending << [ source, true ]
|
|
64
|
+
if source.is_a?(Hash)
|
|
65
|
+
source.each_pair do |key, item|
|
|
66
|
+
pending << [ item, false ] if container?(item)
|
|
67
|
+
pending << [ key, false ] if container?(key)
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
source.each { |item| pending << [ item, false ] if container?(item) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
copies.fetch(value)
|
|
74
|
+
end
|
|
75
|
+
private_class_method :copy_deep
|
|
76
|
+
|
|
77
|
+
def self.snapshot(value, copies) = container?(value) ? copies.fetch(value) : copy_value(value, copies, 0)
|
|
78
|
+
private_class_method :snapshot
|
|
79
|
+
|
|
80
|
+
def self.container?(value) = value.is_a?(Hash) || value.is_a?(Array)
|
|
81
|
+
private_class_method :container?
|
|
17
82
|
end
|
|
18
83
|
end
|
data/lib/ctx_parser/input.rb
CHANGED
|
@@ -19,13 +19,16 @@ module CtxParser
|
|
|
19
19
|
raise EncodingError, "CLI output encoding is invalid" unless value.valid_encoding?
|
|
20
20
|
|
|
21
21
|
value = value.encode(Encoding::UTF_8) if encoding == :transcode
|
|
22
|
-
|
|
22
|
+
# UTF-16/32 等编码不兼容 ASCII 字面量;仅在临时视图上检查逻辑字符。
|
|
23
|
+
logical = value.encoding.ascii_compatible? ? value : value.encode(Encoding::UTF_8)
|
|
24
|
+
raise ArgumentError, "CLI output contains a NUL byte" if !allow_nul && logical.include?("\0")
|
|
23
25
|
|
|
24
|
-
case newlines
|
|
25
|
-
when :lf then
|
|
26
|
-
when :delete_cr then
|
|
27
|
-
else value
|
|
26
|
+
normalized = case newlines
|
|
27
|
+
when :lf then logical.gsub(/\r\n?/, "\n")
|
|
28
|
+
when :delete_cr then logical.delete("\r")
|
|
29
|
+
else return value
|
|
28
30
|
end
|
|
31
|
+
normalized.encoding == value.encoding ? normalized : normalized.encode(value.encoding)
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
end
|
data/lib/ctx_parser/parser.rb
CHANGED
|
@@ -3,36 +3,26 @@
|
|
|
3
3
|
module CtxParser
|
|
4
4
|
# 提供公共命令解析契约;厂商语法、编码与完整性由子类负责。
|
|
5
5
|
class Parser
|
|
6
|
-
attr_reader :cursor, :blocks, :document
|
|
7
|
-
|
|
8
6
|
# 每次调用使用新实例解析,避免共享来源状态。
|
|
9
7
|
def self.parse(text, complete: false)
|
|
10
8
|
new.parse(text, complete: complete)
|
|
11
9
|
end
|
|
12
10
|
|
|
13
|
-
#
|
|
11
|
+
# 校验输入及完整性,随后执行命令解析器。
|
|
14
12
|
def parse(text, complete: false)
|
|
15
|
-
|
|
16
|
-
raise ArgumentError, "Runtime output must be text" unless text.is_a?(String)
|
|
13
|
+
raise ArgumentError, "Parser input must be text" unless text.is_a?(String)
|
|
17
14
|
raise ArgumentError, "Completeness must be boolean" unless [ true, false ].include?(complete)
|
|
18
15
|
|
|
19
16
|
parse_output(text, complete: complete)
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
# 序列化诊断元数据,不包含原始来源文本。
|
|
23
|
-
def as_json(*) = { parser: self.class.name
|
|
20
|
+
def as_json(*) = { parser: self.class.name }
|
|
24
21
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
25
22
|
def inspect = "#<#{self.class.name}>"
|
|
26
23
|
|
|
27
24
|
private
|
|
28
25
|
|
|
29
|
-
# 每次解析创建私有游标与配置树;序列化结果不包含来源正文。
|
|
30
|
-
def read_configuration(text, comments: nil, boundaries: nil, document_class: Document)
|
|
31
|
-
@document = document_class.new(text, comments: comments, boundaries: boundaries)
|
|
32
|
-
@cursor, @blocks = document.cursor, document.roots
|
|
33
|
-
@blocks
|
|
34
|
-
end
|
|
35
|
-
|
|
36
26
|
# 解释当前命令回显,保留不支持或格式错误的证据。
|
|
37
27
|
def parse_output(text, complete:)
|
|
38
28
|
raise NotImplementedError, "Command parsers must implement parse_output"
|
|
@@ -42,5 +32,4 @@ end
|
|
|
42
32
|
|
|
43
33
|
# 在依赖可能触发 Rails 自动加载此命名空间前,先定义基础类。
|
|
44
34
|
require_relative "result"
|
|
45
|
-
require_relative "
|
|
46
|
-
require_relative "document"
|
|
35
|
+
require_relative "results"
|
data/lib/ctx_parser/profile.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "commands"
|
|
4
4
|
|
|
5
5
|
module CtxParser
|
|
6
6
|
# 组合平台命令策略,仅覆盖已确认的型号差异。
|
|
@@ -16,7 +16,7 @@ module CtxParser
|
|
|
16
16
|
raise ArgumentError, "Device model must be text" unless model.nil? || model.is_a?(String)
|
|
17
17
|
|
|
18
18
|
@model = model&.strip&.downcase&.freeze
|
|
19
|
-
@model_parsers =
|
|
19
|
+
@model_parsers = model_parser_overrides
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# 返回当前平台策略声明的命令目录。
|
|
@@ -26,16 +26,16 @@ module CtxParser
|
|
|
26
26
|
def parsers = self.class::PARSERS
|
|
27
27
|
|
|
28
28
|
# 保留选择顺序并归组共享视图,不混用不同命令的正文。
|
|
29
|
-
def queries(
|
|
30
|
-
unless
|
|
29
|
+
def queries(dimensions)
|
|
30
|
+
unless dimensions.is_a?(Array) && dimensions.uniq == dimensions && (dimensions - commands.keys).empty?
|
|
31
31
|
raise ArgumentError, "Query dimensions must be unique supported names"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
dimensions_by_command = dimensions.each_with_object({}) do |name, queries|
|
|
35
35
|
commands_for(name).each { |command| (queries[command] ||= []) << name }
|
|
36
36
|
end
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
dimensions_by_command.each_value(&:freeze)
|
|
38
|
+
dimensions_by_command.freeze
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# 单个维度可能需要多个独立回显;调用方必须分别保存正文。
|
|
@@ -69,21 +69,22 @@ module CtxParser
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# AP、Radio 等共享视图仅解析一次,确保状态与数量证据一致。
|
|
72
|
-
def parse_many(
|
|
73
|
-
unless
|
|
72
|
+
def parse_many(dimensions, text, complete: false, command: nil)
|
|
73
|
+
unless dimensions.is_a?(Array) && dimensions.any? && dimensions.uniq == dimensions
|
|
74
74
|
raise ArgumentError, "Parser dimensions must be a nonempty unique array"
|
|
75
75
|
end
|
|
76
|
-
query = select_command(
|
|
76
|
+
query = select_command(dimensions, command)
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
# 查询已覆盖全部请求维度,无需再次遍历第一个维度的完整命令列表。
|
|
79
|
+
result = @model_parsers.fetch(query) { parsers.fetch(query) }.parse(text, complete: complete)
|
|
80
|
+
dimensions.to_h { |name| [ name, project_independently(name, result) ] }
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
private
|
|
83
84
|
|
|
84
85
|
# 仅当存在唯一公共查询时允许推断;显式命令必须覆盖全部请求视图。
|
|
85
|
-
def select_command(
|
|
86
|
-
candidates =
|
|
86
|
+
def select_command(dimensions, selected)
|
|
87
|
+
candidates = dimensions.map { |name| commands_for(name) }.reduce { |common, queries| common & queries }
|
|
87
88
|
unless selected.nil?
|
|
88
89
|
raise ArgumentError, "Selected command does not provide all parser dimensions" unless candidates.include?(selected)
|
|
89
90
|
|
|
@@ -96,7 +97,7 @@ module CtxParser
|
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
# 采集前校验所选型号,并隔离后续修改对型号查找的影响。
|
|
99
|
-
def
|
|
100
|
+
def model_parser_overrides
|
|
100
101
|
declarations = self.class::MODEL_PARSERS
|
|
101
102
|
raise ArgumentError, "Model parsers must be a hash" unless declarations.is_a?(Hash)
|
|
102
103
|
|
|
@@ -112,12 +113,12 @@ module CtxParser
|
|
|
112
113
|
|
|
113
114
|
# 投影失败仅影响单个视图;命令选择及解析错误仍正常抛出。
|
|
114
115
|
def project_independently(name, result)
|
|
115
|
-
return failed_projection(:invalid_parser_result) unless result.is_a?(Result) || result.is_a?(
|
|
116
|
+
return failed_projection(:invalid_parser_result) unless result.is_a?(Result) || result.is_a?(Results)
|
|
116
117
|
|
|
117
118
|
projected = project(name, result)
|
|
118
119
|
projected.is_a?(Result) ? projected : failed_projection(:invalid_parser_result)
|
|
119
120
|
rescue KeyError
|
|
120
|
-
code = result.is_a?(
|
|
121
|
+
code = result.is_a?(Results) && !result.key?(name) ? :missing_parser_view : :parser_projection_failed
|
|
121
122
|
failed_projection(code)
|
|
122
123
|
rescue StandardError
|
|
123
124
|
failed_projection(:parser_projection_failed)
|
|
@@ -128,7 +129,7 @@ module CtxParser
|
|
|
128
129
|
|
|
129
130
|
# 从共享命令结果中提取请求维度。
|
|
130
131
|
def project(name, result)
|
|
131
|
-
result.is_a?(
|
|
132
|
+
result.is_a?(Results) ? result.fetch(name) : result
|
|
132
133
|
end
|
|
133
134
|
end
|
|
134
135
|
end
|
data/lib/ctx_parser/records.rb
CHANGED
|
@@ -6,21 +6,21 @@ module CtxParser
|
|
|
6
6
|
attr_reader :rows, :first_occurrences, :unambiguous, :duplicate_identity_count, :duplicate_row_count
|
|
7
7
|
|
|
8
8
|
# 冻结筛选结果数组,记录值的所有权仍属于调用方。
|
|
9
|
-
def initialize(rows, identity: nil, &
|
|
9
|
+
def initialize(rows, identity: nil, &identity_key)
|
|
10
10
|
unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) }
|
|
11
11
|
raise ArgumentError, "Records must be hashes"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
raise ArgumentError, "Identity must be a nonempty array or callable" unless
|
|
14
|
+
identity_key ||= identity if identity.respond_to?(:call)
|
|
15
|
+
identity_key ||= ->(row) { row.values_at(*identity) } if identity.is_a?(Array) && identity.any?
|
|
16
|
+
raise ArgumentError, "Identity must be a nonempty array or callable" unless identity_key
|
|
17
17
|
|
|
18
18
|
@rows = rows.dup.freeze
|
|
19
|
-
|
|
20
|
-
@first_occurrences =
|
|
21
|
-
@unambiguous =
|
|
22
|
-
@duplicate_identity_count =
|
|
23
|
-
@duplicate_row_count = @rows.size -
|
|
19
|
+
identity_groups = @rows.group_by { |row| identity_key.call(row) }.values
|
|
20
|
+
@first_occurrences = identity_groups.map(&:first).freeze
|
|
21
|
+
@unambiguous = identity_groups.filter_map { |group| group.first if group.one? }.freeze
|
|
22
|
+
@duplicate_identity_count = identity_groups.count { |group| group.size > 1 }
|
|
23
|
+
@duplicate_row_count = @rows.size - identity_groups.size
|
|
24
24
|
freeze
|
|
25
25
|
end
|
|
26
26
|
|
data/lib/ctx_parser/registry.rb
CHANGED
|
@@ -7,6 +7,10 @@ module CtxParser
|
|
|
7
7
|
class Registry
|
|
8
8
|
# 注册平台策略及其声明的别名。
|
|
9
9
|
def initialize(profiles:, aliases: {})
|
|
10
|
+
unless profiles.is_a?(Hash) && aliases.is_a?(Hash)
|
|
11
|
+
raise ArgumentError, "Registry declarations must be hashes"
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
@profiles = profiles.transform_keys { |key| platform_key(key) }.freeze
|
|
11
15
|
@aliases = aliases.transform_keys { |key| platform_key(key) }.transform_values { |key| platform_key(key) }.freeze
|
|
12
16
|
unless @profiles.size == profiles.size && @aliases.size == aliases.size &&
|
data/lib/ctx_parser/result.rb
CHANGED
|
@@ -9,6 +9,7 @@ module CtxParser
|
|
|
9
9
|
def initialize(status:, records: [], issues: [])
|
|
10
10
|
raise ArgumentError, "Invalid parser status" unless %i[complete partial failed unsupported].include?(status)
|
|
11
11
|
raise ArgumentError, "Records must be hashes" unless records.is_a?(Array) && records.all? { |row| row.is_a?(Hash) }
|
|
12
|
+
raise ArgumentError, "Issues must be hashes" unless issues.is_a?(Array) && issues.all? { |issue| issue.is_a?(Hash) }
|
|
12
13
|
|
|
13
14
|
super(status: status, records: Immutable.copy(records), issues: Immutable.copy(issues))
|
|
14
15
|
end
|
|
@@ -18,5 +19,6 @@ module CtxParser
|
|
|
18
19
|
|
|
19
20
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
20
21
|
def inspect = "#<#{self.class.name} status=#{status} records=#{records.size}>"
|
|
22
|
+
alias to_s inspect
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "result"
|
|
4
|
+
|
|
5
|
+
module CtxParser
|
|
6
|
+
# 同一回显可产生多份清单,各自保留完整性与诊断。
|
|
7
|
+
Results = Data.define(:by_dimension) do
|
|
8
|
+
# 冻结具名章节结果,同时保留各章节的独立状态。
|
|
9
|
+
def initialize(by_dimension:)
|
|
10
|
+
unless by_dimension.is_a?(Hash) && by_dimension.any? &&
|
|
11
|
+
by_dimension.keys.all? { |name| name.is_a?(Symbol) && !name.empty? } &&
|
|
12
|
+
by_dimension.values.all? { |result| result.is_a?(Result) }
|
|
13
|
+
raise ArgumentError, "Results must map named dimensions to parser results"
|
|
14
|
+
end
|
|
15
|
+
# 普通映射不继承调用方的默认值或默认回调,缺失维度必须仍然缺失。
|
|
16
|
+
super(by_dimension: by_dimension.to_h { |name, result| [ name, result ] }.freeze)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 返回必需的具名章节结果。
|
|
20
|
+
def fetch(name) = by_dimension.fetch(name)
|
|
21
|
+
|
|
22
|
+
# 只查询明确提供的维度,不把缺失结果当作空清单。
|
|
23
|
+
def key?(name) = by_dimension.key?(name)
|
|
24
|
+
|
|
25
|
+
# 判断所有已选事实是否都有完整证据支持。
|
|
26
|
+
def complete? = by_dimension.each_value.all?(&:complete?)
|
|
27
|
+
|
|
28
|
+
# 推导汇总状态,避免将证据缺失当作成功。
|
|
29
|
+
def status
|
|
30
|
+
return :complete if complete?
|
|
31
|
+
return :partial if by_dimension.each_value.any? { |result| %i[complete partial].include?(result.status) }
|
|
32
|
+
|
|
33
|
+
by_dimension.each_value.all? { |result| result.status == :unsupported } ? :unsupported : :failed
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 从全部章节结果汇总去重后的诊断。
|
|
37
|
+
def issues = by_dimension.each_value.flat_map(&:issues).uniq
|
|
38
|
+
|
|
39
|
+
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
40
|
+
def inspect = "#<#{self.class.name} status=#{status} dimensions=#{by_dimension.keys.join(',')}>"
|
|
41
|
+
alias to_s inspect
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/ctx_parser/values.rb
CHANGED
|
@@ -11,19 +11,24 @@ module CtxParser
|
|
|
11
11
|
|
|
12
12
|
# 解析十进制整数,并校验声明的边界。
|
|
13
13
|
def integer(raw, min: 0, max: nil)
|
|
14
|
+
unless (min.nil? || min.is_a?(Integer)) && (max.nil? || max.is_a?(Integer)) && (min.nil? || max.nil? || min <= max)
|
|
15
|
+
raise ArgumentError, "Invalid integer limits"
|
|
16
|
+
end
|
|
14
17
|
return if raw.nil?
|
|
15
|
-
|
|
18
|
+
unless (raw.is_a?(String) || raw.is_a?(Integer)) && /\A\d+\z/.match?(raw.to_s)
|
|
19
|
+
raise ArgumentError, "Invalid decimal integer"
|
|
20
|
+
end
|
|
16
21
|
|
|
17
|
-
value = Integer(raw
|
|
22
|
+
value = raw.is_a?(Integer) ? raw : Integer(raw, 10)
|
|
18
23
|
raise OutOfRange, "Integer out of range" if (min && value < min) || (max && value > max)
|
|
19
24
|
|
|
20
25
|
value
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
# 归一化 MAC 地址,拒绝格式错误的输入。
|
|
24
|
-
def
|
|
29
|
+
def mac_address(raw)
|
|
25
30
|
return if raw.nil?
|
|
26
|
-
unless /\A(?:[0-9a-f]{12}|
|
|
31
|
+
unless raw.is_a?(String) && /\A(?:[0-9a-f]{12}|[0-9a-f]{4}([-.])[0-9a-f]{4}\1[0-9a-f]{4}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})\z/i.match?(raw)
|
|
27
32
|
raise ArgumentError, "Invalid MAC address"
|
|
28
33
|
end
|
|
29
34
|
|
|
@@ -31,29 +36,32 @@ module CtxParser
|
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
# 归一化主机地址,保留 IPv4 与 IPv6 语义。
|
|
34
|
-
def
|
|
39
|
+
def ip_address(raw, allow_zone: false)
|
|
40
|
+
raise ArgumentError, "Zone permission must be boolean" unless [ true, false ].include?(allow_zone)
|
|
35
41
|
return if raw.nil?
|
|
36
|
-
raise ArgumentError, "Invalid
|
|
42
|
+
raise ArgumentError, "Invalid IP address" unless raw.is_a?(String)
|
|
43
|
+
raise ArgumentError, "Invalid IP address" if raw.include?("/") || (!allow_zone && raw.include?("%"))
|
|
37
44
|
|
|
38
45
|
IPAddr.new(raw).to_s
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
# 校验主机地址有效性,不让格式错误值继续传播。
|
|
42
|
-
def
|
|
43
|
-
!
|
|
49
|
+
def ip_address?(raw, allow_zone: false)
|
|
50
|
+
!ip_address(raw, allow_zone: allow_zone).nil?
|
|
44
51
|
rescue ArgumentError, IPAddr::Error
|
|
45
52
|
false
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
# 统一校验网段与接口地址,仅接口投影保留主机位。
|
|
49
|
-
def ipv4_prefix(raw,
|
|
56
|
+
def ipv4_prefix(raw, preserve_host_bits: false)
|
|
57
|
+
raise ArgumentError, "Host-bit preservation must be boolean" unless [ true, false ].include?(preserve_host_bits)
|
|
50
58
|
unless raw.is_a?(String) && raw.match?(%r{\A\d+(?:\.\d+){3}/\d+\z})
|
|
51
59
|
raise ArgumentError, "IPv4 prefix required"
|
|
52
60
|
end
|
|
53
61
|
network = IPAddr.new(raw)
|
|
54
62
|
raise ArgumentError, "IPv4 prefix required" unless network.ipv4?
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
preserve_host_bits ? raw : "#{network}/#{network.prefix}"
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
end
|
data/lib/ctx_parser/version.rb
CHANGED
data/lib/ctx_parser.rb
CHANGED
|
@@ -8,10 +8,10 @@ end
|
|
|
8
8
|
|
|
9
9
|
require_relative "ctx_parser/parser"
|
|
10
10
|
if CtxParser.const_defined?(:Parser, false)
|
|
11
|
-
require_relative "ctx_parser/
|
|
11
|
+
require_relative "ctx_parser/configuration"
|
|
12
12
|
require_relative "ctx_parser/values"
|
|
13
13
|
require_relative "ctx_parser/records"
|
|
14
|
-
require_relative "ctx_parser/
|
|
14
|
+
require_relative "ctx_parser/commands"
|
|
15
15
|
require_relative "ctx_parser/profile"
|
|
16
16
|
require_relative "ctx_parser/registry"
|
|
17
17
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ctx_parser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -37,7 +37,7 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
-
description: Reusable configuration documents, command
|
|
40
|
+
description: Reusable configuration documents, command declarations, platform profiles,
|
|
41
41
|
immutable results and parsing primitives for network automation.
|
|
42
42
|
executables: []
|
|
43
43
|
extensions: []
|
|
@@ -54,8 +54,8 @@ files:
|
|
|
54
54
|
- lib/ctx_parser/banner.rb
|
|
55
55
|
- lib/ctx_parser/block.rb
|
|
56
56
|
- lib/ctx_parser/capture.rb
|
|
57
|
-
- lib/ctx_parser/
|
|
58
|
-
- lib/ctx_parser/
|
|
57
|
+
- lib/ctx_parser/commands.rb
|
|
58
|
+
- lib/ctx_parser/configuration.rb
|
|
59
59
|
- lib/ctx_parser/cursor.rb
|
|
60
60
|
- lib/ctx_parser/document.rb
|
|
61
61
|
- lib/ctx_parser/immutable.rb
|
|
@@ -66,7 +66,7 @@ files:
|
|
|
66
66
|
- lib/ctx_parser/records.rb
|
|
67
67
|
- lib/ctx_parser/registry.rb
|
|
68
68
|
- lib/ctx_parser/result.rb
|
|
69
|
-
- lib/ctx_parser/
|
|
69
|
+
- lib/ctx_parser/results.rb
|
|
70
70
|
- lib/ctx_parser/values.rb
|
|
71
71
|
- lib/ctx_parser/version.rb
|
|
72
72
|
homepage: https://github.com/gatework/ctx_parser
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "result"
|
|
4
|
-
|
|
5
|
-
module CtxParser
|
|
6
|
-
# 同一回显可产生多份清单,各自保留完整性与诊断。
|
|
7
|
-
ResultSet = Data.define(:results) do
|
|
8
|
-
# 冻结具名章节结果,同时保留各章节的独立状态。
|
|
9
|
-
def initialize(results:)
|
|
10
|
-
unless results.is_a?(Hash) && results.any? && results.values.all? { |result| result.is_a?(Result) }
|
|
11
|
-
raise ArgumentError, "Result set must contain parser results"
|
|
12
|
-
end
|
|
13
|
-
super(results: results.dup.freeze)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# 返回必需的具名章节结果。
|
|
17
|
-
def fetch(name) = results.fetch(name)
|
|
18
|
-
|
|
19
|
-
# 判断所有已选事实是否都有完整证据支持。
|
|
20
|
-
def complete? = results.values.all?(&:complete?)
|
|
21
|
-
|
|
22
|
-
# 推导汇总状态,避免将证据缺失当作成功。
|
|
23
|
-
def status
|
|
24
|
-
return :complete if complete?
|
|
25
|
-
return :partial if results.values.any? { |result| %i[complete partial].include?(result.status) }
|
|
26
|
-
|
|
27
|
-
results.values.all? { |result| result.status == :unsupported } ? :unsupported : :failed
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# 从全部章节结果汇总去重后的诊断。
|
|
31
|
-
def issues = results.values.flat_map(&:issues).uniq
|
|
32
|
-
|
|
33
|
-
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
34
|
-
def inspect = "#<#{self.class.name} status=#{status} sections=#{results.keys.join(',')}>"
|
|
35
|
-
end
|
|
36
|
-
end
|