ctx_parser 0.2.1 → 0.2.3
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 +17 -0
- data/README-zh.md +12 -2
- data/README.md +12 -2
- data/lib/ctx_parser/block.rb +3 -2
- data/lib/ctx_parser/capture.rb +2 -1
- data/lib/ctx_parser/cursor.rb +10 -6
- data/lib/ctx_parser/document.rb +8 -2
- data/lib/ctx_parser/immutable.rb +23 -1
- data/lib/ctx_parser/line.rb +7 -3
- data/lib/ctx_parser/profile.rb +93 -20
- data/lib/ctx_parser/registry.rb +2 -1
- data/lib/ctx_parser/result.rb +3 -0
- data/lib/ctx_parser/results.rb +3 -0
- data/lib/ctx_parser/values.rb +4 -3
- data/lib/ctx_parser/version.rb +1 -1
- data/lib/ctx_parser.rb +2 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c642815fd4891349acb98052cf1601b9b69217f4025db17d02c64705506eca12
|
|
4
|
+
data.tar.gz: 91e34bd39b1116a3228c9653dd51322bc0ac8d47dec25e3fcf94a41af947ed92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9cd537e0188eda9578bfb03e5a1cc26d703a62da5bbd1cbc2beb4fcce66233ab03de5959f20cc9821b7780ab5a2c4edc764fa063cc8afb5ca7148cf9ffd6f955
|
|
7
|
+
data.tar.gz: '022948b2a66b03ba946c748cf5aaf5dfca9eff72710f859147e8c69e195de7e1b7f055eff54c21e28eb83d763ce3c1128520c726ae3eabd655fa0f943cc91b88'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.2.3 (2026-10-18)
|
|
6
|
+
|
|
7
|
+
- Redact Result, Results and Line during pretty printing, including nested containers.
|
|
8
|
+
- Normalize model override names, reject normalized collisions, and validate static registry command/parser consistency.
|
|
9
|
+
- Report missing dynamic parser implementations as argument errors; retain dynamic command lookup.
|
|
10
|
+
- Accept signed decimal integers when bounds permit negative values, and detach host-bit-preserving prefix results from caller strings.
|
|
11
|
+
- Add optional byte budgets to Cursor and Document, load Banner from the root, and remove the source-type circular require.
|
|
12
|
+
- Reuse validated deeply frozen plain containers and frozen source strings while preserving cycle detection and defensive copies for mutable values.
|
|
13
|
+
- Require ipaddr 1.2.3 or newer for strict address parsing; retain the Ruby 4.0.6 support policy.
|
|
14
|
+
|
|
15
|
+
## 0.2.2 (2026-10-18)
|
|
16
|
+
|
|
17
|
+
- Preserve exact block, capture and unresolved source text independently of Ruby's global output separator.
|
|
18
|
+
- Normalize source lines independently of Ruby's global input separator, without an intermediate chomp allocation.
|
|
19
|
+
- Build cursor lines directly during scanning instead of retaining an intermediate source-line array.
|
|
20
|
+
- Return false from IP address validation for incompatible string encodings.
|
|
21
|
+
|
|
5
22
|
## 0.2.1 (2026-10-17)
|
|
6
23
|
|
|
7
24
|
- Update the configuration benchmark to use the Baseline 0.2 vendor parser namespace.
|
data/README-zh.md
CHANGED
|
@@ -21,7 +21,7 @@ gem "ctx_parser"
|
|
|
21
21
|
|
|
22
22
|
然后执行 `bundle install`;也可直接执行 `gem install ctx_parser`。Ruby 加载入口为 `ctx_parser`,公开命名空间为 `CtxParser`;gem 包名与 Ruby 命名空间分别维护。
|
|
23
23
|
|
|
24
|
-
本模块不依赖其他 Gatework 领域 gem。 gemspec
|
|
24
|
+
本模块不依赖其他 Gatework 领域 gem。 gemspec 要求 `ipaddr >= 1.2.3` 以确保严格地址解析,`digest` 不设版本约束;应用应提交自己的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
|
|
25
25
|
|
|
26
26
|
发布产物和校验值见 [GitHub Releases](https://github.com/gatework/ctx_parser/releases),已发布版本以 [RubyGems](https://rubygems.org/gems/ctx_parser) 为准。
|
|
27
27
|
|
|
@@ -70,7 +70,7 @@ result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
|
70
70
|
|
|
71
71
|
`Parser.parse(text, complete: false)` 返回包含 `status`、`records`、`issues` 的 `Result`;多视图解析器可返回 `Results`。状态为 `complete`、`partial`、`failed`、`unsupported`。子类实现 `parse_output`,配置子类通过 `analyze` 和 `capture` 保留来源。
|
|
72
72
|
|
|
73
|
-
`complete: true` 只表示调用方确认输入完整接收,不证明所有语法都已理解。未知行、身份冲突及空表证据不足必须明确表达。结果的记录和诊断均须为 Hash 数组;结果及构建完成的文档诊断持有不可变快照。`inspect` 和
|
|
73
|
+
`complete: true` 只表示调用方确认输入完整接收,不证明所有语法都已理解。未知行、身份冲突及空表证据不足必须明确表达。结果的记录和诊断均须为 Hash 数组;结果及构建完成的文档诊断持有不可变快照。`inspect`、`to_s` 和 pretty printing 不输出结果正文与诊断内容,显式读取记录仍返回原始事实。来源游标有意保留输入原文,含秘密时不可写入日志。
|
|
74
74
|
|
|
75
75
|
本 gem 不提供厂商命令目录、传输、持久化或健康策略。参数和注册表配置错误保持异常,由调用方在对应边界处理。
|
|
76
76
|
|
|
@@ -84,6 +84,12 @@ result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
|
84
84
|
|
|
85
85
|
地址转换使用 `Values.ip_address`、`Values.ip_address?`、`Values.mac_address`。`Values.ipv4_prefix(..., preserve_host_bits: true)` 明确保留接口主机位,默认返回网段前缀。
|
|
86
86
|
|
|
87
|
+
`Cursor.new(text, max_bytes: limit)` 和 `Document.new(source, max_bytes: limit)` 可在构建或消费来源行前限制输入字节数。传入已有游标时,限制针对其归一化内容。非法编码仍抛出错误;已知为 Latin-1 的输入可先标记 `Encoding::ISO_8859_1`,随后自动转为 UTF-8,不静默替换字节并生成事实。
|
|
88
|
+
|
|
89
|
+
`MODEL_PARSERS` 的型号键与请求型号一样去除两侧空白并转为小写,归一化冲突和无效覆盖均被拒绝。`Registry` 在注册时校验静态命令与解析器目录;动态目录在使用时仍需校验,缺失实现抛出 `ArgumentError`。
|
|
90
|
+
|
|
91
|
+
`Records` 持有自己的筛选数组,行对象仍属于调用方;使用期间应保持行不变,需要快照时传入 `Immutable.copy(rows)`。`Immutable.copy` 在逐层校验后可复用深冻结普通容器;浅冻结、特殊 Hash 行为及循环引用仍按原规则复制或拒绝。整数转换仅在 `min` 允许负数或为 `nil` 时接受负号,默认仍只接受非负数。
|
|
92
|
+
|
|
87
93
|
## 开发与测试
|
|
88
94
|
|
|
89
95
|
共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
|
|
@@ -99,6 +105,10 @@ bundle exec rake build
|
|
|
99
105
|
|
|
100
106
|
使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
|
|
101
107
|
|
|
108
|
+
这些开发命令须在源码检出目录执行;测试、基准和脚本有意排除在安装包之外。
|
|
109
|
+
|
|
110
|
+
执行 `bundle exec ruby -Ilib benchmark/snapshots.rb` 可测量冻结结果的重复封装;执行 `bundle exec ruby -Ilib benchmark/source.rb`,可使用合成输入和语义断言测量游标及文档构建的耗时与对象分配量,无需领域 gem。此类本地测量不代表设备性能。
|
|
111
|
+
|
|
102
112
|
## 发布
|
|
103
113
|
|
|
104
114
|
本目录可独立维护为 [ctx_parser](https://github.com/gatework/ctx_parser) 仓库。更新 `VERSION` 和 `CHANGELOG.md`,运行上述验证与构建命令,检查包内文件,再创建 `vVERSION` 标签。
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ gem "ctx_parser"
|
|
|
21
21
|
|
|
22
22
|
Then run `bundle install`, or install directly with `gem install ctx_parser`. The require path is `ctx_parser` and the public namespace is `CtxParser`; package names and Ruby namespaces are intentionally separate.
|
|
23
23
|
|
|
24
|
-
This package has no dependency on the other Gatework domain gems. The gemspec
|
|
24
|
+
This package has no dependency on the other Gatework domain gems. The gemspec requires `ipaddr >= 1.2.3` for strict address parsing; `digest` has no version constraint. Applications should commit their own `Gemfile.lock` to make deployments reproducible. No Rails boot or sibling checkout paths are required.
|
|
25
25
|
|
|
26
26
|
Release artifacts and checksums are available from [GitHub Releases](https://github.com/gatework/ctx_parser/releases). Check [RubyGems](https://rubygems.org/gems/ctx_parser) for published versions.
|
|
27
27
|
|
|
@@ -70,7 +70,7 @@ result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
|
70
70
|
|
|
71
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. Result records and issues must be arrays of hashes. Results and completed document diagnostics own immutable snapshots. `inspect` and
|
|
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`, `to_s`, and pretty printing 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
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
|
|
|
@@ -84,6 +84,12 @@ A dimension is a named view of facts; a command is the CLI query that supplies t
|
|
|
84
84
|
|
|
85
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
86
|
|
|
87
|
+
`Cursor.new(text, max_bytes: limit)` and `Document.new(source, max_bytes: limit)` enforce an optional input byte budget before constructing or consuming lines. For an existing cursor, the budget applies to its normalized content. Invalid encoding remains an error: known Latin-1 input can be tagged as `Encoding::ISO_8859_1` before parsing and is transcoded to UTF-8; bytes are never silently scrubbed into facts.
|
|
88
|
+
|
|
89
|
+
`MODEL_PARSERS` model names are stripped and lowercased just like the requested model. Normalization collisions and invalid overrides are rejected. `Registry` validates static command/parser declarations when registering a profile; dynamic directory lookup remains validated when used and reports missing implementations as `ArgumentError`.
|
|
90
|
+
|
|
91
|
+
`Records` owns selection arrays, while row objects remain caller-owned: treat those rows as stable during use, or pass `Immutable.copy(rows)` when a snapshot is needed. `Immutable.copy` can reuse fully frozen plain trees after validating all descendants; shallow-frozen containers, custom Hash behavior, and cycles receive the existing copy or rejection behavior. Integer conversion accepts a minus sign only when `min` permits negative values (or is `nil`); the default remains nonnegative.
|
|
92
|
+
|
|
87
93
|
## Development and tests
|
|
88
94
|
|
|
89
95
|
Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
|
|
@@ -99,6 +105,10 @@ bundle exec rake build
|
|
|
99
105
|
|
|
100
106
|
Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
|
|
101
107
|
|
|
108
|
+
Development commands below require a source checkout; tests, benchmarks, and scripts are intentionally excluded from the installed gem archive.
|
|
109
|
+
|
|
110
|
+
Run `bundle exec ruby -Ilib benchmark/snapshots.rb` to measure repeated wrapping of frozen results, and `bundle exec ruby -Ilib benchmark/source.rb` to measure cursor and document construction with synthetic input and semantic checks. It needs no domain gems and reports elapsed time and allocated objects; these local measurements do not establish device performance.
|
|
111
|
+
|
|
102
112
|
## Release
|
|
103
113
|
|
|
104
114
|
This directory can be maintained as the independent [ctx_parser](https://github.com/gatework/ctx_parser) repository. Update `VERSION` and `CHANGELOG.md`, run the validation and build commands above, and inspect the packaged file list before tagging `vVERSION`.
|
data/lib/ctx_parser/block.rb
CHANGED
|
@@ -23,6 +23,7 @@ module CtxParser
|
|
|
23
23
|
child.index > previous_index && child.last_index <= last_index && child.header.indent > header.indent
|
|
24
24
|
|
|
25
25
|
previous_index = child.last_index
|
|
26
|
+
true
|
|
26
27
|
end
|
|
27
28
|
raise ArgumentError, "Child blocks must be ordered within their parent source"
|
|
28
29
|
end
|
|
@@ -88,7 +89,7 @@ module CtxParser
|
|
|
88
89
|
|
|
89
90
|
# 返回配置块标题的零起始索引。
|
|
90
91
|
def index = header.index
|
|
91
|
-
#
|
|
92
|
+
# 返回配置块标题从 1 开始的来源行号。
|
|
92
93
|
def line = header.number
|
|
93
94
|
# 返回已去除外围缩进和行结束符的来源内容。
|
|
94
95
|
def text = header.text
|
|
@@ -97,7 +98,7 @@ module CtxParser
|
|
|
97
98
|
# 返回属于当前配置块的原始来源行。
|
|
98
99
|
def lines = cursor.lines[index..last_index].freeze
|
|
99
100
|
# 返回配置块或捕获事实对应的精确来源文本。
|
|
100
|
-
def content = lines.map(&:raw).join.freeze
|
|
101
|
+
def content = lines.map(&:raw).join("").freeze
|
|
101
102
|
# 返回当前配置块各来源行的处理状态。
|
|
102
103
|
def states = lines.map { |source| cursor.state(source.index) }.freeze
|
|
103
104
|
# 要求块内所有来源行均已解析或明确忽略。
|
data/lib/ctx_parser/capture.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "block"
|
|
4
3
|
require_relative "immutable"
|
|
5
4
|
|
|
6
5
|
module CtxParser
|
|
@@ -22,6 +21,7 @@ module CtxParser
|
|
|
22
21
|
|
|
23
22
|
# 冻结解析事实及其来源块和结果路径。
|
|
24
23
|
def initialize(block:, data:, path:)
|
|
24
|
+
require_relative "block" unless defined?(Block)
|
|
25
25
|
raise ArgumentError, "Capture source must be a block" unless block.is_a?(Block)
|
|
26
26
|
|
|
27
27
|
initialize_snapshot(block, Snapshot.new(data: data, path: path))
|
|
@@ -29,6 +29,7 @@ module CtxParser
|
|
|
29
29
|
|
|
30
30
|
# 同一事实的多个来源共享已复制的值,不共享未经校验的调用方容器。
|
|
31
31
|
def self.for_blocks(blocks, data:, path:)
|
|
32
|
+
require_relative "block" unless defined?(Block)
|
|
32
33
|
unless blocks.is_a?(Array) && blocks.all? { |block| block.is_a?(Block) }
|
|
33
34
|
raise ArgumentError, "Capture sources must be blocks"
|
|
34
35
|
end
|
data/lib/ctx_parser/cursor.rb
CHANGED
|
@@ -14,14 +14,17 @@ module CtxParser
|
|
|
14
14
|
attr_reader :content, :lines, :checksum, :position
|
|
15
15
|
|
|
16
16
|
# 为单次解析创建独立的来源行、语义状态及事实索引。
|
|
17
|
-
def initialize(content)
|
|
17
|
+
def initialize(content, max_bytes: nil)
|
|
18
18
|
raise ArgumentError, "Configuration content must be text" unless content.is_a?(String)
|
|
19
19
|
|
|
20
|
-
@content = Input.text(content, encoding: :transcode).freeze
|
|
20
|
+
@content = Input.text(content, encoding: :transcode, max_bytes: max_bytes).freeze
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# scan 直接交付每行,避免同时保留原始行数组与 Line 数组。
|
|
23
|
+
@lines = []
|
|
24
|
+
@content.scan(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+\z/) do |raw|
|
|
25
|
+
@lines << Line.new(index: @lines.size, raw: raw.freeze)
|
|
26
|
+
end
|
|
27
|
+
@lines.freeze
|
|
25
28
|
@checksum = Digest::SHA256.hexdigest(@content).freeze
|
|
26
29
|
reset!
|
|
27
30
|
end
|
|
@@ -123,7 +126,7 @@ module CtxParser
|
|
|
123
126
|
end
|
|
124
127
|
|
|
125
128
|
# 拼接未解决来源行,并保留原有格式。
|
|
126
|
-
def unparsed_content = unparsed_lines.map(&:raw).join.freeze
|
|
129
|
+
def unparsed_content = unparsed_lines.map(&:raw).join("").freeze
|
|
127
130
|
|
|
128
131
|
# 将事实快照关联到来源块,不将整个块标记为解析成功。
|
|
129
132
|
def capture(block, data:, path:)
|
|
@@ -132,6 +135,7 @@ module CtxParser
|
|
|
132
135
|
|
|
133
136
|
# 注册归纳事实的不可变快照前,先校验全部来源。
|
|
134
137
|
def capture_many(blocks, data:, path:)
|
|
138
|
+
require_relative "block" unless defined?(Block)
|
|
135
139
|
unless blocks.is_a?(Array) && blocks.all? { |block| block.is_a?(Block) && block.cursor.equal?(self) }
|
|
136
140
|
raise ArgumentError, "Blocks must belong to this cursor"
|
|
137
141
|
end
|
data/lib/ctx_parser/document.rb
CHANGED
|
@@ -9,8 +9,14 @@ module CtxParser
|
|
|
9
9
|
attr_reader :cursor, :roots, :issues
|
|
10
10
|
|
|
11
11
|
# 使用平台提供的注释及边界规则构建来源文档。
|
|
12
|
-
def initialize(source, comments: nil, boundaries: nil)
|
|
13
|
-
|
|
12
|
+
def initialize(source, comments: nil, boundaries: nil, max_bytes: nil)
|
|
13
|
+
if source.is_a?(Cursor)
|
|
14
|
+
unless max_bytes.nil? || max_bytes.is_a?(Integer) && max_bytes >= 0
|
|
15
|
+
raise ArgumentError, "Invalid byte limit"
|
|
16
|
+
end
|
|
17
|
+
raise ArgumentError, "CLI output exceeds byte limit" if max_bytes && source.content.bytesize > max_bytes
|
|
18
|
+
end
|
|
19
|
+
@cursor = source.is_a?(Cursor) ? source : Cursor.new(source, max_bytes: max_bytes)
|
|
14
20
|
@comments, @boundaries, @issues = comments, boundaries, []
|
|
15
21
|
@roots = Block.read(cursor) { |line| classify(line) }
|
|
16
22
|
finish
|
data/lib/ctx_parser/immutable.rb
CHANGED
|
@@ -23,6 +23,10 @@ module CtxParser
|
|
|
23
23
|
return copy_deep(value, copies) if depth == 64
|
|
24
24
|
|
|
25
25
|
copies[value] = COPYING
|
|
26
|
+
if reusable?(value, copies, depth)
|
|
27
|
+
copies[value] = value
|
|
28
|
+
return value
|
|
29
|
+
end
|
|
26
30
|
copies[value] = if value.is_a?(Hash)
|
|
27
31
|
value.to_h { |key, item| [ copy_value(key, copies, depth + 1), copy_value(item, copies, depth + 1) ] }.freeze
|
|
28
32
|
else
|
|
@@ -40,13 +44,31 @@ module CtxParser
|
|
|
40
44
|
end
|
|
41
45
|
private_class_method :copy_value
|
|
42
46
|
|
|
47
|
+
# 仅复用无特殊 Hash 行为的深冻结普通容器;逐层验证,不信任外层 frozen?。
|
|
48
|
+
def self.reusable?(value, copies, depth)
|
|
49
|
+
return false unless value.frozen? && value.instance_variables.empty?
|
|
50
|
+
if value.instance_of?(Array)
|
|
51
|
+
value.all? { |item| copy_value(item, copies, depth + 1).equal?(item) }
|
|
52
|
+
elsif value.instance_of?(Hash) && value.default.nil? && !value.default_proc && !value.compare_by_identity?
|
|
53
|
+
value.each_pair do |key, item|
|
|
54
|
+
return false unless copy_value(key, copies, depth + 1).equal?(key) && copy_value(item, copies, depth + 1).equal?(item)
|
|
55
|
+
end
|
|
56
|
+
true
|
|
57
|
+
else
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
private_class_method :reusable?
|
|
62
|
+
|
|
43
63
|
# 容器缓存按来源身份查找,普通 Hash 的键仍沿用值相等语义。
|
|
44
64
|
def self.copy_deep(value, copies)
|
|
45
65
|
pending = [ [ value, false ] ]
|
|
46
66
|
until pending.empty?
|
|
47
67
|
source, finishing = pending.pop
|
|
48
68
|
if finishing
|
|
49
|
-
copies[source] = if
|
|
69
|
+
copies[source] = if reusable?(source, copies, 0)
|
|
70
|
+
source
|
|
71
|
+
elsif source.is_a?(Hash)
|
|
50
72
|
source.to_h { |key, item| [ snapshot(key, copies), snapshot(item, copies) ] }.freeze
|
|
51
73
|
else
|
|
52
74
|
source.map { |item| snapshot(item, copies) }.freeze
|
data/lib/ctx_parser/line.rb
CHANGED
|
@@ -8,12 +8,13 @@ module CtxParser
|
|
|
8
8
|
raise ArgumentError, "Line index must be nonnegative" unless index.is_a?(Integer) && index >= 0
|
|
9
9
|
raise ArgumentError, "Line content must be text" unless raw.is_a?(String)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# strip 已移除行结束空白,避免 chomp 依赖宿主进程的 $/。
|
|
12
|
+
@text = raw.strip.freeze
|
|
12
13
|
@indent = raw[/\A[ \t]*/].size
|
|
13
|
-
super(index: index, raw: raw.dup.freeze)
|
|
14
|
+
super(index: index, raw: raw.frozen? ? raw : raw.dup.freeze)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
#
|
|
17
|
+
# 返回诊断使用的从 1 开始的行号。
|
|
17
18
|
def number = index + 1
|
|
18
19
|
# 返回已去除外围缩进和行结束符的来源内容。
|
|
19
20
|
def text = @text
|
|
@@ -24,5 +25,8 @@ module CtxParser
|
|
|
24
25
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
25
26
|
def inspect = "#<#{self.class.name} number=#{number}>"
|
|
26
27
|
alias to_s inspect
|
|
28
|
+
|
|
29
|
+
# PP 不得绕过 inspect 暴露来源行。
|
|
30
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
27
31
|
end
|
|
28
32
|
end
|
data/lib/ctx_parser/profile.rb
CHANGED
|
@@ -17,8 +17,39 @@ module CtxParser
|
|
|
17
17
|
|
|
18
18
|
@model = model&.strip&.downcase&.freeze
|
|
19
19
|
@model_parsers = model_parser_overrides
|
|
20
|
+
@static_commands = self.class::COMMANDS
|
|
21
|
+
@command_queries = compile_commands(@static_commands)
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# 注册时核对静态目录;动态实例仍在实际查询时重新校验。
|
|
25
|
+
def self.validate_declarations!
|
|
26
|
+
directory, implementations = self::COMMANDS, self::PARSERS
|
|
27
|
+
unless directory.is_a?(Hash) && implementations.is_a?(Hash) &&
|
|
28
|
+
directory.keys.all? { |name| name.is_a?(Symbol) && !name.empty? }
|
|
29
|
+
raise ArgumentError, "Profile commands and parsers must be named mappings"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
queries = directory.values.flat_map do |value|
|
|
33
|
+
entries = value.is_a?(Array) ? value : [ value ]
|
|
34
|
+
unless entries.any? && entries.uniq == entries && entries.all? { |entry| valid_command?(entry) }
|
|
35
|
+
raise ArgumentError, "Parser dimension must declare one or more unique commands"
|
|
36
|
+
end
|
|
37
|
+
entries
|
|
38
|
+
end.uniq
|
|
39
|
+
unless (queries - implementations.keys).empty? && (implementations.keys - queries).empty? &&
|
|
40
|
+
implementations.values.all? { |parser| parser.is_a?(Class) && parser < Parser }
|
|
41
|
+
raise ArgumentError, "Profile commands must match Parser implementations"
|
|
42
|
+
end
|
|
43
|
+
normalized_model_parsers
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 校验静态目录中的单命令文本,不接受控制字符。
|
|
48
|
+
def self.valid_command?(value)
|
|
49
|
+
value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
|
|
50
|
+
end
|
|
51
|
+
private_class_method :valid_command?
|
|
52
|
+
|
|
22
53
|
# 返回当前平台策略声明的命令目录。
|
|
23
54
|
def commands = self.class::COMMANDS
|
|
24
55
|
|
|
@@ -40,13 +71,11 @@ module CtxParser
|
|
|
40
71
|
|
|
41
72
|
# 单个维度可能需要多个独立回显;调用方必须分别保存正文。
|
|
42
73
|
def commands_for(name)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
raise ArgumentError, "Parser dimension must declare one or more unique commands"
|
|
47
|
-
end
|
|
74
|
+
directory = commands
|
|
75
|
+
declaration = directory.fetch(name) { raise ArgumentError, "Unsupported parser dimension" }
|
|
76
|
+
return @command_queries.fetch(name) if @command_queries && directory.equal?(@static_commands)
|
|
48
77
|
|
|
49
|
-
|
|
78
|
+
validated_queries(declaration)
|
|
50
79
|
end
|
|
51
80
|
|
|
52
81
|
# 解析支持的观测维度对应的命令。
|
|
@@ -60,7 +89,7 @@ module CtxParser
|
|
|
60
89
|
# 确定维度使用的解析器,包括命令特定变体。
|
|
61
90
|
def parser_for(name, command: nil)
|
|
62
91
|
query = select_command([ name ], command)
|
|
63
|
-
|
|
92
|
+
implementation_for(query)
|
|
64
93
|
end
|
|
65
94
|
|
|
66
95
|
# 选择目标命令解析器,并投影单个请求维度。
|
|
@@ -76,39 +105,83 @@ module CtxParser
|
|
|
76
105
|
query = select_command(dimensions, command)
|
|
77
106
|
|
|
78
107
|
# 查询已覆盖全部请求维度,无需再次遍历第一个维度的完整命令列表。
|
|
79
|
-
result =
|
|
108
|
+
result = implementation_for(query).parse(text, complete: complete)
|
|
80
109
|
dimensions.to_h { |name| [ name, project_independently(name, result) ] }
|
|
81
110
|
end
|
|
82
111
|
|
|
83
112
|
private
|
|
84
113
|
|
|
114
|
+
# 仅为深冻结的静态目录建立快照;可变或被替换的目录仍走实时校验。
|
|
115
|
+
def compile_commands(directory)
|
|
116
|
+
return unless directory.instance_of?(Hash) && directory.frozen? && !directory.compare_by_identity? &&
|
|
117
|
+
directory.keys.all? { |name| name.is_a?(Symbol) && !name.empty? } && directory.values.all? do |value|
|
|
118
|
+
entries = value.is_a?(Array) ? value : [ value ]
|
|
119
|
+
(!value.is_a?(Array) || value.frozen?) && entries.all? { |query| query.is_a?(String) && query.frozen? }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
directory.to_h { |name, declaration| [ name, validated_queries(declaration) ] }.freeze
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# 查询快照不冻结调用方的数组或字符串。
|
|
126
|
+
def validated_queries(declaration)
|
|
127
|
+
queries = declaration.is_a?(Array) ? declaration : [ declaration ]
|
|
128
|
+
unless queries.any? && queries.uniq == queries && queries.all? { |query| query.is_a?(String) && !query.strip.empty? && !/[\x00-\x1f\x7f]/.match?(query) }
|
|
129
|
+
raise ArgumentError, "Parser dimension must declare one or more unique commands"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
queries.map { |query| query.frozen? ? query : query.dup.freeze }.freeze
|
|
133
|
+
end
|
|
134
|
+
|
|
85
135
|
# 仅当存在唯一公共查询时允许推断;显式命令必须覆盖全部请求视图。
|
|
86
136
|
def select_command(dimensions, selected)
|
|
87
137
|
candidates = dimensions.map { |name| commands_for(name) }.reduce { |common, queries| common & queries }
|
|
88
|
-
|
|
89
|
-
raise ArgumentError, "Selected command does not provide all parser dimensions" unless candidates.include?(selected)
|
|
90
|
-
|
|
91
|
-
selected
|
|
92
|
-
else
|
|
138
|
+
if selected.nil?
|
|
93
139
|
raise ArgumentError, "One CLI output must belong to one command" unless candidates.one?
|
|
94
140
|
|
|
95
141
|
candidates.first
|
|
142
|
+
else
|
|
143
|
+
raise ArgumentError, "Selected command does not provide all parser dimensions" unless candidates.include?(selected)
|
|
144
|
+
|
|
145
|
+
selected
|
|
96
146
|
end
|
|
97
147
|
end
|
|
98
148
|
|
|
99
149
|
# 采集前校验所选型号,并隔离后续修改对型号查找的影响。
|
|
100
|
-
def
|
|
101
|
-
declarations = self
|
|
150
|
+
def self.normalized_model_parsers
|
|
151
|
+
declarations = self::MODEL_PARSERS
|
|
102
152
|
raise ArgumentError, "Model parsers must be a hash" unless declarations.is_a?(Hash)
|
|
103
153
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
normalized = {}
|
|
155
|
+
declarations.each do |name, overrides|
|
|
156
|
+
unless name.nil? || name.is_a?(String) && !name.strip.empty?
|
|
157
|
+
raise ArgumentError, "Model parser keys must be device model names"
|
|
107
158
|
end
|
|
108
|
-
|
|
159
|
+
key = name&.strip&.downcase
|
|
160
|
+
raise ArgumentError, "Model parser names must be unique after normalization" if normalized.key?(key)
|
|
161
|
+
unless overrides.is_a?(Hash) && overrides.all? do |command, parser|
|
|
162
|
+
self::PARSERS.key?(command) && parser.is_a?(Class) && parser < Parser
|
|
163
|
+
end
|
|
164
|
+
raise ArgumentError, "Model parsers must override declared commands with Parser subclasses"
|
|
165
|
+
end
|
|
166
|
+
normalized[key] = overrides
|
|
109
167
|
end
|
|
110
168
|
|
|
111
|
-
|
|
169
|
+
normalized
|
|
170
|
+
end
|
|
171
|
+
private_class_method :normalized_model_parsers
|
|
172
|
+
|
|
173
|
+
# 注册与实例选择共用声明校验,所选覆盖仍独立于后续调用方修改。
|
|
174
|
+
def model_parser_overrides
|
|
175
|
+
self.class.send(:normalized_model_parsers).fetch(model, {}).dup.freeze
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# 动态目录的错误仍在调用边界报告,不暴露底层 Hash 的 KeyError。
|
|
179
|
+
def implementation_for(query)
|
|
180
|
+
implementation = @model_parsers.fetch(query) { parsers.fetch(query) { raise ArgumentError, "Missing command parser" } }
|
|
181
|
+
unless implementation.is_a?(Class) && implementation < Parser
|
|
182
|
+
raise ArgumentError, "Command parser must inherit from Parser"
|
|
183
|
+
end
|
|
184
|
+
implementation
|
|
112
185
|
end
|
|
113
186
|
|
|
114
187
|
# 投影失败仅影响单个视图;命令选择及解析错误仍正常抛出。
|
data/lib/ctx_parser/registry.rb
CHANGED
|
@@ -18,9 +18,10 @@ module CtxParser
|
|
|
18
18
|
@aliases.values.all? { |key| @profiles.key?(key) } && (@profiles.keys & @aliases.keys).empty?
|
|
19
19
|
raise ArgumentError, "Invalid parser registry"
|
|
20
20
|
end
|
|
21
|
+
@profiles.each_value(&:validate_declarations!)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
+
# 按平台或别名创建 Profile 实例,拒绝未知名称。
|
|
24
25
|
def fetch(platform, model: nil)
|
|
25
26
|
key = platform_key(platform)
|
|
26
27
|
key = @aliases.fetch(key, key)
|
data/lib/ctx_parser/result.rb
CHANGED
data/lib/ctx_parser/results.rb
CHANGED
|
@@ -39,5 +39,8 @@ module CtxParser
|
|
|
39
39
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
40
40
|
def inspect = "#<#{self.class.name} status=#{status} dimensions=#{by_dimension.keys.join(',')}>"
|
|
41
41
|
alias to_s inspect
|
|
42
|
+
|
|
43
|
+
# 具名结果的嵌套展示同样不能展开事实或诊断。
|
|
44
|
+
def pretty_print(printer) = printer.text(inspect)
|
|
42
45
|
end
|
|
43
46
|
end
|
data/lib/ctx_parser/values.rb
CHANGED
|
@@ -15,7 +15,8 @@ module CtxParser
|
|
|
15
15
|
raise ArgumentError, "Invalid integer limits"
|
|
16
16
|
end
|
|
17
17
|
return if raw.nil?
|
|
18
|
-
|
|
18
|
+
pattern = min.nil? || min.negative? ? /\A-?\d+\z/ : /\A\d+\z/
|
|
19
|
+
unless (raw.is_a?(String) || raw.is_a?(Integer)) && pattern.match?(raw.to_s)
|
|
19
20
|
raise ArgumentError, "Invalid decimal integer"
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -48,7 +49,7 @@ module CtxParser
|
|
|
48
49
|
# 校验主机地址有效性,不让格式错误值继续传播。
|
|
49
50
|
def ip_address?(raw, allow_zone: false)
|
|
50
51
|
!ip_address(raw, allow_zone: allow_zone).nil?
|
|
51
|
-
rescue ArgumentError, IPAddr::Error
|
|
52
|
+
rescue ArgumentError, IPAddr::Error, EncodingError
|
|
52
53
|
false
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -61,7 +62,7 @@ module CtxParser
|
|
|
61
62
|
network = IPAddr.new(raw)
|
|
62
63
|
raise ArgumentError, "IPv4 prefix required" unless network.ipv4?
|
|
63
64
|
|
|
64
|
-
preserve_host_bits ? raw : "#{network}/#{network.prefix}"
|
|
65
|
+
preserve_host_bits ? raw.dup : "#{network}/#{network.prefix}"
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
end
|
data/lib/ctx_parser/version.rb
CHANGED
data/lib/ctx_parser.rb
CHANGED
|
@@ -7,6 +7,7 @@ module CtxParser
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
require_relative "ctx_parser/parser"
|
|
10
|
+
# 直接加载 parser 时,命名空间 autoload 可能重入此入口;此时 Parser 尚未定义。
|
|
10
11
|
if CtxParser.const_defined?(:Parser, false)
|
|
11
12
|
require_relative "ctx_parser/configuration"
|
|
12
13
|
require_relative "ctx_parser/values"
|
|
@@ -14,4 +15,5 @@ if CtxParser.const_defined?(:Parser, false)
|
|
|
14
15
|
require_relative "ctx_parser/commands"
|
|
15
16
|
require_relative "ctx_parser/profile"
|
|
16
17
|
require_relative "ctx_parser/registry"
|
|
18
|
+
require_relative "ctx_parser/banner"
|
|
17
19
|
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.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
32
|
+
version: 1.2.3
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version:
|
|
39
|
+
version: 1.2.3
|
|
40
40
|
description: Reusable configuration documents, command declarations, platform profiles,
|
|
41
41
|
immutable results and parsing primitives for network automation.
|
|
42
42
|
executables: []
|