ctx_parser 0.2.4 → 0.2.6
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 +15 -0
- data/README-zh.md +3 -1
- data/README.md +3 -1
- data/docs/input-performance.md +19 -0
- data/examples/offline.rb +1 -1
- data/lib/ctx_parser/immutable.rb +6 -2
- data/lib/ctx_parser/input.rb +7 -3
- data/lib/ctx_parser/profile.rb +42 -7
- data/lib/ctx_parser/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 488dca1bfd052afe8a7719c6e5c89d4b2b5c86f189243c02b651438ce45a69c1
|
|
4
|
+
data.tar.gz: 33e7aa374cc8285c6dbb76ff4c9d87849116c3fa1a61ddb1ee720d108baed8ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8459a636b16bdbe34e4446530714b1f686daa9bab80ca0cb713fdff9422066a388a7410dae7d083126c92415c8cb97f479a321da91d6113feb947cd03158bff6
|
|
7
|
+
data.tar.gz: ae3de9681c49c1d4be422c3b4b7c1fc47aeb0c152cb740370846fc3f80122435b184de6f2c977c3dac06d848c04a2c2d7191ad54d286214eee027c67d0b48abc
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.2.6 (2026-10-21)
|
|
8
|
+
|
|
9
|
+
- Build immutable hash snapshots without temporary field-pair arrays, retaining ordinary key equality, cycle rejection and deep-tree support.
|
|
10
|
+
|
|
11
|
+
- Add repository-specific AGENTS.md guidance for responsibilities, Ruby conventions, evidence boundaries and validation.
|
|
12
|
+
- Snapshot requested profile views before parser callbacks can mutate caller selections.
|
|
13
|
+
- Avoid redundant UTF-8 conversion and line-ending replacement for already normalized inputs while preserving mutable detached return values.
|
|
14
|
+
|
|
15
|
+
## 0.2.5 (2026-10-19)
|
|
16
|
+
|
|
17
|
+
- Keep offline example facts and configuration benchmark inputs independent of Ruby's global input and output separators.
|
|
18
|
+
- Reuse validated dynamic command directories and membership indexes when their complete contents are frozen, avoiding repeated full-directory validation during discovered-query preparation. Mutable and replaced directories retain current lookup semantics.
|
|
19
|
+
|
|
5
20
|
## 0.2.4 (2026-10-18)
|
|
6
21
|
|
|
7
22
|
- Clear Bundler bootstrap variables when verifying packages in an isolated gem installation, including calls made through bundle exec.
|
data/README-zh.md
CHANGED
|
@@ -46,7 +46,7 @@ class ExampleStatusParser < CtxParser::Parser
|
|
|
46
46
|
def parse_output(text, complete:)
|
|
47
47
|
records = []
|
|
48
48
|
issues = []
|
|
49
|
-
text.each_line.with_index(1) do |line, number|
|
|
49
|
+
text.each_line("\n").with_index(1) do |line, number|
|
|
50
50
|
next if line.strip.empty?
|
|
51
51
|
|
|
52
52
|
match = /\A([A-Za-z0-9\/]+) (up|down)\z/.match(line.strip)
|
|
@@ -88,6 +88,8 @@ result.records.each { |row| puts "#{row[:name]}: #{row[:state]}" }
|
|
|
88
88
|
|
|
89
89
|
`MODEL_PARSERS` 的型号键与请求型号一样去除两侧空白并转为小写,归一化冲突和无效覆盖均被拒绝。`Registry` 在注册时校验静态命令与解析器目录;动态目录在使用时仍需校验,缺失实现抛出 `ArgumentError`。
|
|
90
90
|
|
|
91
|
+
Profile 为深冻结的普通命令目录复用查询列表与成员索引,包括发现阶段产生的目录。替换目录后重建索引;可变条目仍在每次查找时校验。`bundle exec ruby -Ilib benchmark/profile.rb` 测量 32、128、512 条合成命令的准备开销,并校验选择顺序与未知命令拒绝行为。
|
|
92
|
+
|
|
91
93
|
`Records` 持有自己的筛选数组,行对象仍属于调用方;使用期间应保持行不变,需要快照时传入 `Immutable.copy(rows)`。`Immutable.copy` 在逐层校验后可复用深冻结普通容器;浅冻结、特殊 Hash 行为及循环引用仍按原规则复制或拒绝。整数转换仅在 `min` 允许负数或为 `nil` 时接受负号,默认仍只接受非负数。
|
|
92
94
|
|
|
93
95
|
## 开发与测试
|
data/README.md
CHANGED
|
@@ -46,7 +46,7 @@ class ExampleStatusParser < CtxParser::Parser
|
|
|
46
46
|
def parse_output(text, complete:)
|
|
47
47
|
records = []
|
|
48
48
|
issues = []
|
|
49
|
-
text.each_line.with_index(1) do |line, number|
|
|
49
|
+
text.each_line("\n").with_index(1) do |line, number|
|
|
50
50
|
next if line.strip.empty?
|
|
51
51
|
|
|
52
52
|
match = /\A([A-Za-z0-9\/]+) (up|down)\z/.match(line.strip)
|
|
@@ -88,6 +88,8 @@ Address conversion uses `Values.ip_address`, `Values.ip_address?`, and `Values.m
|
|
|
88
88
|
|
|
89
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
90
|
|
|
91
|
+
Profiles reuse query lists and membership indexes for fully frozen plain command directories, including directories created by discovery. Replacing a directory rebuilds its index; mutable entries are validated on each lookup. `bundle exec ruby -Ilib benchmark/profile.rb` measures preparation of 32, 128 and 512 synthetic commands and checks selection order and rejection of unknown commands.
|
|
92
|
+
|
|
91
93
|
`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
94
|
|
|
93
95
|
## Development and tests
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 输入归一化验证(2026-10-21)
|
|
2
|
+
|
|
3
|
+
`Input.text` 的常见路径原先会对 UTF-8 输入再次执行 `encode(UTF_8)`,并在 LF 正文上执行无匹配的 `gsub`。现在跳过相同编码的转换,在没有 CR 时跳过替换;需要修改时只操作归一化过程拥有的副本。返回值仍可变且独立于调用方;原文、编码策略、逻辑 NUL 校验和转码前字节预算不变。
|
|
4
|
+
|
|
5
|
+
在 macOS arm64、Ruby 4.0.6 上使用 `benchmark/input.rb`,对修改前的 `b8746d9` 与本次源码执行相同脚本。每轮重复归一化同一正文 30 次,共三轮;语义和对象所有权断言在计时外执行。下表是三轮中位数,属于重复输入的本地微基准,不代表设备采集、完整配置解析或网络吞吐。
|
|
6
|
+
|
|
7
|
+
| 正文 | 输入字节 | 修改前耗时 | 修改后耗时 | 修改前分配 | 修改后分配 |
|
|
8
|
+
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
9
|
+
| UTF-8 / LF | 950000 | 67.354 ms | 0.866 ms | 120 | 30 |
|
|
10
|
+
| UTF-8 / CRLF | 1000000 | 177.798 ms | 162.056 ms | 150 | 150 |
|
|
11
|
+
| ISO-8859-1 / LF | 850000 | 169.909 ms | 90.748 ms | 150 | 60 |
|
|
12
|
+
|
|
13
|
+
复测当前代码:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bundle exec ruby -Ilib benchmark/input.rb
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
比较基线时,把目标提交的 `lib/ctx_parser/input.rb` 放入独立临时目录的 `ctx_parser/input.rb`,然后使用 `bundle exec ruby -I<临时目录> benchmark/input.rb`。脚本输出实际加载的 Input 来源,避免误测已安装版本。不要覆盖工作区文件来切换基线。
|
data/examples/offline.rb
CHANGED
|
@@ -10,7 +10,7 @@ class ExampleStatusParser < CtxParser::Parser
|
|
|
10
10
|
def parse_output(text, complete:)
|
|
11
11
|
records = []
|
|
12
12
|
issues = []
|
|
13
|
-
text.each_line.with_index(1) do |line, number|
|
|
13
|
+
text.each_line("\n").with_index(1) do |line, number|
|
|
14
14
|
next if line.strip.empty?
|
|
15
15
|
|
|
16
16
|
match = /\A([A-Za-z0-9\/]+) (up|down)\z/.match(line.strip)
|
data/lib/ctx_parser/immutable.rb
CHANGED
|
@@ -28,7 +28,9 @@ module CtxParser
|
|
|
28
28
|
return value
|
|
29
29
|
end
|
|
30
30
|
copies[value] = if value.is_a?(Hash)
|
|
31
|
-
|
|
31
|
+
result = {}
|
|
32
|
+
value.each_pair { |key, item| result[copy_value(key, copies, depth + 1)] = copy_value(item, copies, depth + 1) }
|
|
33
|
+
result.freeze
|
|
32
34
|
else
|
|
33
35
|
value.map { |item| copy_value(item, copies, depth + 1) }.freeze
|
|
34
36
|
end
|
|
@@ -69,7 +71,9 @@ module CtxParser
|
|
|
69
71
|
copies[source] = if reusable?(source, copies, 0)
|
|
70
72
|
source
|
|
71
73
|
elsif source.is_a?(Hash)
|
|
72
|
-
|
|
74
|
+
result = {}
|
|
75
|
+
source.each_pair { |key, item| result[snapshot(key, copies)] = snapshot(item, copies) }
|
|
76
|
+
result.freeze
|
|
73
77
|
else
|
|
74
78
|
source.map { |item| snapshot(item, copies) }.freeze
|
|
75
79
|
end
|
data/lib/ctx_parser/input.rb
CHANGED
|
@@ -18,14 +18,18 @@ module CtxParser
|
|
|
18
18
|
value.force_encoding(Encoding::UTF_8) if encoding == :utf8 || value.encoding == Encoding::BINARY
|
|
19
19
|
raise EncodingError, "CLI output encoding is invalid" unless value.valid_encoding?
|
|
20
20
|
|
|
21
|
-
value = value.encode(Encoding::UTF_8) if encoding == :transcode
|
|
21
|
+
value = value.encode(Encoding::UTF_8) if encoding == :transcode && value.encoding != Encoding::UTF_8
|
|
22
22
|
# UTF-16/32 等编码不兼容 ASCII 字面量;仅在临时视图上检查逻辑字符。
|
|
23
23
|
logical = value.encoding.ascii_compatible? ? value : value.encode(Encoding::UTF_8)
|
|
24
24
|
raise ArgumentError, "CLI output contains a NUL byte" if !allow_nul && logical.include?("\0")
|
|
25
25
|
|
|
26
26
|
normalized = case newlines
|
|
27
|
-
when :lf
|
|
28
|
-
|
|
27
|
+
when :lf
|
|
28
|
+
logical.gsub!(/\r\n?/, "\n") if logical.include?("\r")
|
|
29
|
+
logical
|
|
30
|
+
when :delete_cr
|
|
31
|
+
logical.delete!("\r")
|
|
32
|
+
logical
|
|
29
33
|
else return value
|
|
30
34
|
end
|
|
31
35
|
normalized.encoding == value.encoding ? normalized : normalized.encode(value.encoding)
|
data/lib/ctx_parser/profile.rb
CHANGED
|
@@ -17,8 +17,7 @@ module CtxParser
|
|
|
17
17
|
|
|
18
18
|
@model = model&.strip&.downcase&.freeze
|
|
19
19
|
@model_parsers = model_parser_overrides
|
|
20
|
-
|
|
21
|
-
@command_queries = compile_commands(@static_commands)
|
|
20
|
+
cache_commands(self.class::COMMANDS, strict: true)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
# 注册时核对静态目录;动态实例仍在实际查询时重新校验。
|
|
@@ -73,7 +72,8 @@ module CtxParser
|
|
|
73
72
|
def commands_for(name)
|
|
74
73
|
directory = commands
|
|
75
74
|
declaration = directory.fetch(name) { raise ArgumentError, "Unsupported parser dimension" }
|
|
76
|
-
|
|
75
|
+
cached = cache_commands(directory)
|
|
76
|
+
return cached[:queries].fetch(name) if cached
|
|
77
77
|
|
|
78
78
|
validated_queries(declaration)
|
|
79
79
|
end
|
|
@@ -102,6 +102,8 @@ module CtxParser
|
|
|
102
102
|
unless dimensions.is_a?(Array) && dimensions.any? && dimensions.uniq == dimensions
|
|
103
103
|
raise ArgumentError, "Parser dimensions must be a nonempty unique array"
|
|
104
104
|
end
|
|
105
|
+
# 扩展解析器可调用外部代码;结果投影始终使用入口处的请求。
|
|
106
|
+
dimensions = dimensions.dup.freeze
|
|
105
107
|
query = select_command(dimensions, command)
|
|
106
108
|
|
|
107
109
|
# 查询已覆盖全部请求维度,无需再次遍历第一个维度的完整命令列表。
|
|
@@ -111,12 +113,32 @@ module CtxParser
|
|
|
111
113
|
|
|
112
114
|
private
|
|
113
115
|
|
|
114
|
-
#
|
|
116
|
+
# 发现阶段可替换实例目录;只缓存当前深冻结目录,不继承旧目录的选择结果。
|
|
117
|
+
def cache_commands(directory, strict: false)
|
|
118
|
+
cached = @command_cache
|
|
119
|
+
return cached if cached && directory.equal?(cached[:directory])
|
|
120
|
+
return if frozen?
|
|
121
|
+
|
|
122
|
+
queries = compile_commands(directory)
|
|
123
|
+
return unless queries
|
|
124
|
+
|
|
125
|
+
membership = queries.transform_values { |entries| entries.to_h { |query| [ query, true ] }.freeze }.freeze
|
|
126
|
+
# 一次发布完整索引;并发查找只读取自己取得的快照,不混合两份目录。
|
|
127
|
+
@command_cache = { directory: directory, queries: queries, membership: membership }.freeze
|
|
128
|
+
rescue ArgumentError
|
|
129
|
+
# 动态目录仍按请求维度报错,未使用的无效声明不能阻断其他维度。
|
|
130
|
+
raise if strict
|
|
131
|
+
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# 可变容器或文本仍逐次校验;普通深冻结目录允许共享验证后的顺序与成员索引。
|
|
115
136
|
def compile_commands(directory)
|
|
116
137
|
return unless directory.instance_of?(Hash) && directory.frozen? && !directory.compare_by_identity? &&
|
|
117
138
|
directory.keys.all? { |name| name.is_a?(Symbol) && !name.empty? } && directory.values.all? do |value|
|
|
118
139
|
entries = value.is_a?(Array) ? value : [ value ]
|
|
119
|
-
(!value.is_a?(Array) || value.
|
|
140
|
+
(!value.is_a?(Array) || value.instance_of?(Array) && value.frozen?) &&
|
|
141
|
+
entries.all? { |query| query.instance_of?(String) && query.frozen? }
|
|
120
142
|
end
|
|
121
143
|
|
|
122
144
|
directory.to_h { |name, declaration| [ name, validated_queries(declaration) ] }.freeze
|
|
@@ -134,13 +156,26 @@ module CtxParser
|
|
|
134
156
|
|
|
135
157
|
# 仅当存在唯一公共查询时允许推断;显式命令必须覆盖全部请求视图。
|
|
136
158
|
def select_command(dimensions, selected)
|
|
137
|
-
|
|
159
|
+
declarations = dimensions.map do |name|
|
|
160
|
+
queries = commands_for(name)
|
|
161
|
+
cached = @command_cache
|
|
162
|
+
membership = cached[:membership][name] if cached && queries.equal?(cached[:queries][name])
|
|
163
|
+
[ queries, membership ]
|
|
164
|
+
end
|
|
138
165
|
if selected.nil?
|
|
166
|
+
candidates = declarations.map(&:first).reduce { |common, queries| common & queries }
|
|
139
167
|
raise ArgumentError, "One CLI output must belong to one command" unless candidates.one?
|
|
140
168
|
|
|
141
169
|
candidates.first
|
|
142
170
|
else
|
|
143
|
-
|
|
171
|
+
supported = declarations.all? do |queries, membership|
|
|
172
|
+
if selected.instance_of?(String) && membership
|
|
173
|
+
membership.key?(selected)
|
|
174
|
+
else
|
|
175
|
+
queries.include?(selected)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
raise ArgumentError, "Selected command does not provide all parser dimensions" unless supported
|
|
144
179
|
|
|
145
180
|
selected
|
|
146
181
|
end
|
data/lib/ctx_parser/version.rb
CHANGED
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- README-zh.md
|
|
49
49
|
- README.md
|
|
50
50
|
- docs/architecture-zh.md
|
|
51
|
+
- docs/input-performance.md
|
|
51
52
|
- docs/release.md
|
|
52
53
|
- examples/offline.rb
|
|
53
54
|
- lib/ctx_parser.rb
|