json-mask 0.1.0 → 0.2.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: b68741ab4daac730737fe2b7a0f0550409d981fe7535619d2c2655a1c1a9beba
4
- data.tar.gz: f65a88df55f70fcb6c2af28f97a805a0dd2a0fbd34c4d2d0975b685946ef1d53
3
+ metadata.gz: bf429554b940fea4a030589830ad3eb3ad509983d365f19c0c371c5ff026b530
4
+ data.tar.gz: 8ff525f2b92a3c4f8634c4c28b96f2dc54d2652a0b5a760f267cf3b7f9885627
5
5
  SHA512:
6
- metadata.gz: 46daa73f26f18e75dc6f1cc83232e33a047326801f38807c5fa2df8f16c94fdebef63778a2acce086aeb75446cb613d700d5f11dc4c9b09f7aa3e2c088b83189
7
- data.tar.gz: 1ef0a561a43eb13ca3e877a7f1dde1590125fb38ff51d609e35f0bc8ee119278144cb3575cbaea19af14e08bcb742c6d183886dfe9f21edf270c9edc7981bb43
6
+ metadata.gz: fb6a7be61059829fb6219b874fcfa41369070aecbd4792889e70a63a6d3c9aeb0b45b727a2cd80c6a2ee4297d695790d72b8c61fc668a1b2c73db64388805537
7
+ data.tar.gz: 52f4c5e63190f7fe5cc238eb13b65b86e93696470d5a0b34704b14e7c46f814b76cf37fb5eea90c02d2d5befa600beaa209f7405f44d4c1289f2f04be8f39d41
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 - Unreleased
3
+ ## 0.2.0 - 2026-09-04
4
+
5
+ - Expose compiled selectors for inspection. `CompiledMask#selection_tree` returns the root
6
+ `JsonMask::SelectionTree`; its `named`, `wildcard`, and `selection_for` readers and
7
+ `JsonMask::Selection`'s `leaf?` and `children` are public API. Applications that have a
8
+ response schema can use the tree to reject selectors that name undeclared fields.
9
+
10
+ ## 0.1.0 - 2026-08-04
4
11
 
5
12
  - Implement Google partial-response and JSON Mask field selectors.
6
13
  - Support comma-separated fields, slash paths, sub-selections, wildcards, and escaping.
data/README.md CHANGED
@@ -116,7 +116,68 @@ Exceeding a limit raises `JsonMask::LimitError`, a subclass of `JsonMask::ParseE
116
116
 
117
117
  Validation is syntactic. Because the library has no response schema, a well-formed selector that
118
118
  names a field absent from the input simply omits that field; it cannot produce Google's
119
- schema-aware "Invalid field selection" error on its own.
119
+ schema-aware "Invalid field selection" error on its own. An application that has a schema can
120
+ produce one by inspecting the compiled selector.
121
+
122
+ ## Inspecting a compiled selector
123
+
124
+ `JsonMask::CompiledMask#selection_tree` exposes the parsed selector as an immutable tree, for
125
+ callers that need to examine a selector rather than apply it:
126
+
127
+ ```ruby
128
+ tree = JsonMask.compile("id,permissions(role),*/kind").selection_tree
129
+
130
+ tree.named.keys # => ["id", "permissions"]
131
+ tree.named["id"].leaf? # => true
132
+ tree.named["permissions"].children.named.keys # => ["role"]
133
+ tree.wildcard.children.named.keys # => ["kind"]
134
+ tree.selection_for("permissions").children.named.keys # => ["role", "kind"]
135
+ tree.selection_for("other").children.named.keys # => ["kind"]
136
+ ```
137
+
138
+ - A `JsonMask::SelectionTree` holds `named`, a frozen `Hash` from each explicitly written field
139
+ name (unescaped) to a `JsonMask::Selection`, and `wildcard`, the `*` selection at that level or
140
+ `nil`.
141
+ - A `JsonMask::Selection` is either a leaf (`leaf?` is true and the whole value is selected) or
142
+ has `children`, a nested `JsonMask::SelectionTree`.
143
+ - `selection_for(key)` returns what the projector applies to a key (`String` or `Symbol`): the
144
+ named selection merged with the wildcard, the wildcard alone for a key that is not named, or
145
+ `nil` when the key is not selected.
146
+
147
+ Blank selectors compile to a mask that passes values through, and its `selection_tree` is `nil`.
148
+
149
+ This is enough to add schema-aware validation. For example, to list the selected paths that a
150
+ JSON-Schema-style hash does not declare:
151
+
152
+ ```ruby
153
+ def undeclared_paths(tree, schema, path = [])
154
+ schema = schema["items"] if schema["type"] == "array"
155
+ properties = schema.fetch("properties", {})
156
+
157
+ tree.named.flat_map do |name, selection|
158
+ property = properties[name]
159
+ next [(path + [name]).join("/")] unless property
160
+ next [] if selection.leaf?
161
+
162
+ undeclared_paths(selection.children, property, path + [name])
163
+ end
164
+ end
165
+
166
+ schema = {
167
+ "type" => "object",
168
+ "properties" => {
169
+ "id" => {"type" => "string"},
170
+ "permissions" => {
171
+ "type" => "array",
172
+ "items" => {"type" => "object", "properties" => {"role" => {"type" => "string"}}}
173
+ }
174
+ }
175
+ }
176
+
177
+ tree = JsonMask.compile("id,permissions(role,email),owner/name").selection_tree
178
+ undeclared_paths(tree, schema)
179
+ # => ["permissions/email", "owner"]
180
+ ```
120
181
 
121
182
  ## Compatibility
122
183
 
data/lib/json-mask.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Entry file matching the gem name, so Bundler's default require
4
- # (`gem "json-mask"`) resolves without an explicit `require:` option.
3
+ # Bundler's default require for `gem "json-mask"` looks for this file name.
5
4
  require_relative 'json_mask'
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMask
4
- # An immutable, reusable field selector.
5
4
  class CompiledMask
6
5
  attr_reader :fields
7
6
 
7
+ # nil for a blank selector, which passes values through unchanged.
8
+ attr_reader :selection_tree
9
+
8
10
  def initialize(fields, selection_tree)
9
11
  @fields = fields&.dup&.freeze
10
12
  @selection_tree = selection_tree
11
13
  freeze
12
14
  end
13
15
 
14
- # Filters a JSON-compatible value using this selector.
15
- #
16
- # @param value [Hash, Array] response data to filter
17
- # @return [Hash, Array, nil] a structural subset of value
18
16
  def call(value)
19
17
  return value unless @selection_tree
20
18
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMask
4
- # Raised when a field selector does not conform to the supported grammar.
5
4
  class ParseError < ArgumentError
6
5
  attr_reader :expression, :offset, :reason
7
6
 
@@ -14,7 +13,7 @@ module JsonMask
14
13
  end
15
14
  end
16
15
 
17
- # Raised when a field selector exceeds a configured parser limit.
16
+ # A ParseError subclass so one rescue covers malformed and oversized selectors.
18
17
  class LimitError < ParseError
19
18
  end
20
19
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMask
4
- # Compiles a field selector string into an immutable selection tree.
5
4
  class Parser
6
5
  DEFAULT_MAX_LENGTH = 16_384
7
6
  DEFAULT_MAX_DEPTH = 64
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMask
4
- # Applies a compiled selection tree to Hash and Array values.
5
4
  module Projector
6
5
  MISSING = Object.new.freeze
7
6
 
@@ -19,9 +18,8 @@ module JsonMask
19
18
  when Array
20
19
  project_array(value, selection_tree)
21
20
  when nil
22
- # JSON null is a value, not a shape mismatch: a nested selection into
23
- # null keeps the null (matching the reference implementation), so a
24
- # nullable field stays distinguishable from an unselected one.
21
+ # Keep nulls, as the reference implementation does, so a nullable
22
+ # field stays distinguishable from an unselected one.
25
23
  nil
26
24
  else
27
25
  MISSING
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMask
4
- # Represents either a complete field or a field with nested selections.
5
4
  class Selection
6
5
  attr_reader :children
7
6
 
@@ -18,6 +17,7 @@ module JsonMask
18
17
  children.nil?
19
18
  end
20
19
 
20
+ # A leaf already selects the whole value, so it absorbs any nested selection.
21
21
  def merge(other)
22
22
  return self.class.leaf if leaf? || other.leaf?
23
23
 
@@ -25,7 +25,6 @@ module JsonMask
25
25
  end
26
26
  end
27
27
 
28
- # Stores named and wildcard selections, merging repeated selections by union.
29
28
  class SelectionTree
30
29
  attr_reader :named, :wildcard
31
30
 
@@ -57,6 +56,8 @@ module JsonMask
57
56
  )
58
57
  end
59
58
 
59
+ # Unlike `named`, folds the wildcard's nested selections into each named
60
+ # selection; unnamed fields get the wildcard itself.
60
61
  def selection_for(key)
61
62
  @effective_named.fetch(key.to_s, wildcard)
62
63
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMask
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/json_mask.rb CHANGED
@@ -7,25 +7,14 @@ require_relative 'json_mask/parser'
7
7
  require_relative 'json_mask/projector'
8
8
  require_relative 'json_mask/compiled_mask'
9
9
 
10
- # Filters JSON-compatible Ruby values using Google partial-response selectors.
11
10
  module JsonMask
12
11
  class << self
13
- # Filters a JSON-compatible value with a field selector.
14
- #
15
- # @param value [Hash, Array] response data to filter
16
- # @param fields [String, nil] fields selector; blank values pass through unchanged
17
- # @return [Hash, Array, nil] a structural subset of value
18
12
  def call(value, fields, **options)
19
13
  compile(fields, **options).call(value)
20
14
  end
21
15
 
22
16
  alias mask call
23
17
 
24
- # Compiles a selector for reuse across multiple values.
25
- #
26
- # @return [CompiledMask] an immutable selector
27
- # @raise [ParseError] if the selector is malformed
28
- # @raise [LimitError] if the selector exceeds a configured limit
29
18
  def compile(
30
19
  fields,
31
20
  max_length: Parser::DEFAULT_MAX_LENGTH,
@@ -43,5 +32,5 @@ module JsonMask
43
32
  end
44
33
  end
45
34
 
46
- private_constant :Parser, :Projector, :Selection, :SelectionTree
35
+ private_constant :Parser, :Projector
47
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json-mask
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Sheldon