ctx_parser 0.3.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 492a2e200870280a1f4b2d58100e532557ac7f8b92bb3f4a84d7850089ebb80a
4
- data.tar.gz: 4e9055e1be83197785f5fbedd6043a42b2568e377951916cc78921d28d9738be
3
+ metadata.gz: 207ae07b7f1f12d8d8a91ce6ea1a89fc39bb2c566f3d1cebe289ab71012aadd5
4
+ data.tar.gz: 1249139af57dd39b634ad948028af664af72eea4af7e5cb9a0137e4117f8eecd
5
5
  SHA512:
6
- metadata.gz: c855bbd1456d6c6569eee7cf9834ea6914d29c942954836b87a5453386cb117f77b38e717b87a88f4d5c5662705976ca02bdb0391d15978d7c95c7aa2dd57131
7
- data.tar.gz: 6043a601e4d81c530ef7f812ae66d78b61f44444574bed26cf02d7bebca3d8fa29b2f91831ca6eca67b1e1b3d66250d8252ab40a036cde57c86aa4dadb743a0c
6
+ metadata.gz: 43e613f54ffa2b660d9badc6e2ce1195ed9fbf5b3fd864c8b85102c0cfae4eb9aa3cac477c7e7c9e92116d3c3e40234ce58e0e6c08db1757106c78a1c0e976f4
7
+ data.tar.gz: d8a2b623628265babbcaedc0f3d15b29257945917e6e4db13d6958680457c1d4f3028066d416c77ff2c2d29a3da8dde5d7520da920d8a40b8ca3794a7247bf04
data/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@ Changes to the public contract are recorded here. Versions follow Semantic Versi
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.4.0 (2026-10-29)
8
+
9
+ - Add immutable `covered_scopes` to Result, including equality, hashing, pattern matching and `with` semantics.
10
+ - Centralize single-line command validation in `Commands.command_text` and strict UTC timestamp parsing in `Values.timestamp`.
11
+
7
12
  ## 0.3.0 (2026-10-29)
8
13
 
9
14
  - Add `Result#partial?` and `#available?`; reject records on failed or unsupported results.
data/README.md CHANGED
@@ -125,3 +125,5 @@ MIT. See [LICENSE](LICENSE).
125
125
 
126
126
 
127
127
  `Result#partial?` identifies partial parsing; `#available?` is true for complete or partial results. Failed and unsupported results cannot contain records. Availability does not establish completeness or network health.
128
+
129
+ 本次使用 ctx_parser 0.4 系列契约;`Result` 新增 `covered_scopes`,单行命令统一由 `CtxParser::Commands.command_text` 校验。跨模块约定见 [ctx_runner 契约迁移](https://github.com/gatework/ctx_runner/blob/main/docs/contracts-2026-10-29.md)。
@@ -7,6 +7,14 @@ module CtxParser
7
7
  class Commands
8
8
  attr_reader :entries, :by_dimension, :parsers
9
9
 
10
+ # 查询文本的唯一值边界;复制后冻结,不冻结调用方提供的字符串。
11
+ def self.command_text(value)
12
+ unless value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
13
+ raise ArgumentError, "One command without control characters is required"
14
+ end
15
+ value.frozen? ? value : value.dup.freeze
16
+ end
17
+
10
18
  # 校验并冻结受支持的维度到命令声明。
11
19
  def initialize(entries, multi_command: false)
12
20
  unless entries.is_a?(Array) && entries.any?
@@ -36,13 +44,10 @@ module CtxParser
36
44
  dimensions.uniq == dimensions && (@multi_command || dimensions.none? { |name| @by_dimension.key?(name) })
37
45
  raise ArgumentError, "Command dimensions must be unique nonempty symbols"
38
46
  end
39
- unless command.is_a?(String) && !command.strip.empty? && !/[\x00-\x1f\x7f]/.match?(command)
40
- raise ArgumentError, "Command must be nonempty text without control characters"
41
- end
47
+ command = self.class.command_text(command)
42
48
  raise ArgumentError, "Command is already declared" if @parsers.key?(command)
43
49
  raise ArgumentError, "Command parser must inherit from Parser" unless parser.is_a?(Class) && parser < Parser
44
50
 
45
- command = command.dup.freeze
46
51
  dimensions = dimensions.dup.freeze
47
52
  dimensions.each do |name|
48
53
  # 索引数组由本实例持有,构建完成后统一冻结;追加不复制已有命令。
@@ -45,7 +45,10 @@ module CtxParser
45
45
 
46
46
  # 校验静态目录中的单命令文本,不接受控制字符。
47
47
  def self.valid_command?(value)
48
- value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
48
+ Commands.command_text(value)
49
+ true
50
+ rescue ArgumentError
51
+ false
49
52
  end
50
53
  private_class_method :valid_command?
51
54
 
@@ -147,11 +150,11 @@ module CtxParser
147
150
  # 查询快照不冻结调用方的数组或字符串。
148
151
  def validated_queries(declaration)
149
152
  queries = declaration.is_a?(Array) ? declaration : [ declaration ]
150
- unless queries.any? && queries.uniq == queries && queries.all? { |query| query.is_a?(String) && !query.strip.empty? && !/[\x00-\x1f\x7f]/.match?(query) }
153
+ unless queries.any? && queries.uniq == queries
151
154
  raise ArgumentError, "Parser dimension must declare one or more unique commands"
152
155
  end
153
156
 
154
- queries.map { |query| query.frozen? ? query : query.dup.freeze }.freeze
157
+ queries.map { |query| Commands.command_text(query) }.freeze
155
158
  end
156
159
 
157
160
  # 仅当存在唯一公共查询时允许推断;显式命令必须覆盖全部请求视图。
@@ -4,18 +4,21 @@ require_relative "immutable"
4
4
 
5
5
  module CtxParser
6
6
  # 仅保存解析结果;采集命令、时间与证据由调用方管理。
7
- Result = Data.define(:status, :records, :issues) do
7
+ Result = Data.define(:status, :records, :issues, :covered_scopes) do
8
8
  # 冻结解析状态、归一化记录与诊断证据。
9
- def initialize(status:, records: [], issues: [])
9
+ def initialize(status:, records: [], issues: [], covered_scopes: [])
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
12
  raise ArgumentError, "Issues must be hashes" unless issues.is_a?(Array) && issues.all? { |issue| issue.is_a?(Hash) }
13
+ unless covered_scopes.is_a?(Array) && covered_scopes.all? { |scope| scope.is_a?(Hash) } && covered_scopes.uniq == covered_scopes
14
+ raise ArgumentError, "Covered scopes must be unique hashes"
15
+ end
13
16
 
14
- if %i[failed unsupported].include?(status) && records.any?
15
- raise ArgumentError, "Unavailable parser results cannot contain records"
17
+ if %i[failed unsupported].include?(status) && (records.any? || covered_scopes.any?)
18
+ raise ArgumentError, "Unavailable parser results cannot contain records or covered scopes"
16
19
  end
17
20
 
18
- super(status: status, records: Immutable.copy(records), issues: Immutable.copy(issues))
21
+ super(status: status, records: Immutable.copy(records), issues: Immutable.copy(issues), covered_scopes: Immutable.copy(covered_scopes))
19
22
  end
20
23
 
21
24
  # 判断解析结果是否具有完整证据。
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ipaddr"
4
+ require "time"
5
+ require "date"
4
6
 
5
7
  module CtxParser
6
8
  # 严格转换标量;单位及可选字段语法仍由命令解析器负责。
@@ -9,6 +11,19 @@ module CtxParser
9
11
 
10
12
  module_function
11
13
 
14
+ # 严格时刻包含时区;拒绝 Time.iso8601 自动归一化的非法日期或时分秒。
15
+ def timestamp(raw)
16
+ unless raw.is_a?(String) && /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/.match?(raw)
17
+ raise ArgumentError, "Timestamp requires an explicit offset"
18
+ end
19
+ Date.iso8601(raw[0, 10])
20
+ if raw[11, 2].to_i >= 24 || raw[14, 2].to_i >= 60 || raw[17, 2].to_i >= 60 ||
21
+ (offset = /[+-](\d{2}):(\d{2})\z/.match(raw)) && (offset[1].to_i >= 24 || offset[2].to_i >= 60)
22
+ raise ArgumentError, "Invalid timestamp"
23
+ end
24
+ Time.iso8601(raw).getutc.freeze
25
+ end
26
+
12
27
  # 解析十进制整数,并校验声明的边界。
13
28
  def integer(raw, min: 0, max: nil)
14
29
  unless (min.nil? || min.is_a?(Integer)) && (max.nil? || max.is_a?(Integer)) && (min.nil? || max.nil? || min <= max)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CtxParser
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -9,6 +9,34 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: date
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: time
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: digest
14
42
  requirement: !ruby/object:Gem::Requirement