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 +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +62 -1
- data/lib/json-mask.rb +1 -2
- data/lib/json_mask/compiled_mask.rb +3 -5
- data/lib/json_mask/error.rb +1 -2
- data/lib/json_mask/parser.rb +0 -1
- data/lib/json_mask/projector.rb +2 -4
- data/lib/json_mask/selection_tree.rb +3 -2
- data/lib/json_mask/version.rb +1 -1
- data/lib/json_mask.rb +1 -12
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf429554b940fea4a030589830ad3eb3ad509983d365f19c0c371c5ff026b530
|
|
4
|
+
data.tar.gz: 8ff525f2b92a3c4f8634c4c28b96f2dc54d2652a0b5a760f267cf3b7f9885627
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb6a7be61059829fb6219b874fcfa41369070aecbd4792889e70a63a6d3c9aeb0b45b727a2cd80c6a2ee4297d695790d72b8c61fc668a1b2c73db64388805537
|
|
7
|
+
data.tar.gz: 52f4c5e63190f7fe5cc238eb13b65b86e93696470d5a0b34704b14e7c46f814b76cf37fb5eea90c02d2d5befa600beaa209f7405f44d4c1289f2f04be8f39d41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
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
|
-
#
|
|
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
|
|
data/lib/json_mask/error.rb
CHANGED
|
@@ -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
|
-
#
|
|
16
|
+
# A ParseError subclass so one rescue covers malformed and oversized selectors.
|
|
18
17
|
class LimitError < ParseError
|
|
19
18
|
end
|
|
20
19
|
end
|
data/lib/json_mask/parser.rb
CHANGED
data/lib/json_mask/projector.rb
CHANGED
|
@@ -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
|
-
#
|
|
23
|
-
#
|
|
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
|
data/lib/json_mask/version.rb
CHANGED
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
|
|
35
|
+
private_constant :Parser, :Projector
|
|
47
36
|
end
|