params-registry 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.yardopts +2 -0
- data/LICENSE +202 -0
- data/README.md +86 -0
- data/Rakefile +8 -0
- data/lib/params/registry/error.rb +46 -0
- data/lib/params/registry/instance.rb +210 -0
- data/lib/params/registry/template.rb +400 -0
- data/lib/params/registry/types.rb +167 -0
- data/lib/params/registry/version.rb +8 -0
- data/lib/params/registry.rb +403 -0
- data/lib/params-registry.rb +1 -0
- data/params-registry.gemspec +41 -0
- data/sig/params/registry.rbs +6 -0
- metadata +77 -0
@@ -0,0 +1,403 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'registry/version'
|
4
|
+
require_relative 'registry/error'
|
5
|
+
require_relative 'registry/template'
|
6
|
+
require_relative 'registry/instance'
|
7
|
+
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
# {Params::Registry} is intended to contain an organization-wide
|
11
|
+
# registry of reusable named parameters. The initial purpose of such a
|
12
|
+
# thing is to control the lexical representation of serialization
|
13
|
+
# schemes of, e.g., {::URI} query parameters and
|
14
|
+
# `application/x-www-form-urlencoded` Web form input, such that they
|
15
|
+
# can be _round-tripped_ from a canonicalized string representation to
|
16
|
+
# a processed, in-memory representation and back.
|
17
|
+
#
|
18
|
+
class Params::Registry
|
19
|
+
|
20
|
+
# A group is an identifiable sequence of parameters.
|
21
|
+
class Group
|
22
|
+
# Create a new group.
|
23
|
+
#
|
24
|
+
# @param registry [Params::Registry] the registry
|
25
|
+
#
|
26
|
+
def initialize registry, id, templates: nil
|
27
|
+
@id = id
|
28
|
+
@registry = registry
|
29
|
+
@templates = {} # main mapping
|
30
|
+
@aliases = {} # alternate mapping
|
31
|
+
@ranks = {} # internal ranking for dependencies
|
32
|
+
|
33
|
+
templates = (Types::Array|Types::TemplateMap)[templates]
|
34
|
+
|
35
|
+
# use the internal subscript assignment
|
36
|
+
templates.each { |t, spec| self[t] = spec || t }
|
37
|
+
end
|
38
|
+
|
39
|
+
# !@attribute [r] id
|
40
|
+
# @return [Object] the identifier of the group.
|
41
|
+
#
|
42
|
+
# !@attribute [r] registry
|
43
|
+
# @return [Params::Registry] the associated registry.
|
44
|
+
|
45
|
+
attr_reader :id, :registry
|
46
|
+
|
47
|
+
# Retrieve a template.
|
48
|
+
#
|
49
|
+
# @param id [Object] the template identifier, either canonical or an alias.
|
50
|
+
#
|
51
|
+
# @return [Params::Registry::Template, nil] the template, if one is found.
|
52
|
+
#
|
53
|
+
def [] id
|
54
|
+
@templates[id] || @aliases[id]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add a parameter template to the group. The `spec` can be a
|
58
|
+
# template specification, or it can be an already-instantiated
|
59
|
+
# template, or it can be the same as `id`, or it can be `nil`. In
|
60
|
+
# the first case, the template will be created and added to the
|
61
|
+
# registry, replacing any template with the same ID. In the case
|
62
|
+
# that it's a {Params::Registry::Template} instance, its ID must
|
63
|
+
# match `id` and it must come from the same registry as the
|
64
|
+
# group. In the latter two cases, the parameter is retrieved from
|
65
|
+
# the registry, raising an exception if not.
|
66
|
+
#
|
67
|
+
# @param id [Object] the template's canonical identifier.
|
68
|
+
# @param spec [Hash{Symbol => Object}, Params::Registry::Template, nil]
|
69
|
+
# the template specification, as described above.
|
70
|
+
#
|
71
|
+
# @return [Params::Registry::Template] the new template, assigned to
|
72
|
+
# the registry.
|
73
|
+
#
|
74
|
+
def []= id, spec
|
75
|
+
case spec
|
76
|
+
when nil, id
|
77
|
+
template = registry.templates[id]
|
78
|
+
raise ArgumentError, "Could not find template #{id}" unless template
|
79
|
+
when Template
|
80
|
+
raise ArgumentError,
|
81
|
+
"Template #{id} supplied from some other registry" unless
|
82
|
+
registry.equal? spec.registry
|
83
|
+
raise ArgumentError,
|
84
|
+
"Identifier #{id} does not match template (#{spec.id})" unless
|
85
|
+
id == spec.id
|
86
|
+
template = spec
|
87
|
+
else
|
88
|
+
Types::Hash[spec]
|
89
|
+
template = registry.template_class.new registry, id, **spec
|
90
|
+
end
|
91
|
+
|
92
|
+
# amke sure we aren't calling ourselves
|
93
|
+
registry.templates[id] = template unless registry.templates.equal? self
|
94
|
+
|
95
|
+
# okay now actually assign
|
96
|
+
@templates[id] = template
|
97
|
+
|
98
|
+
# then map all the aliases and crap
|
99
|
+
@aliases[template.slug] = template if template.slug
|
100
|
+
# we use a conditional assign here since aliases take a lower priority
|
101
|
+
template.aliases.each { |a| @aliases[a] ||= template }
|
102
|
+
|
103
|
+
# now we compute the rank, but first we need the dependencies
|
104
|
+
deps = template.depends.map do |t|
|
105
|
+
registry.templates[t]
|
106
|
+
end.compact.map(&:id)
|
107
|
+
|
108
|
+
# warn deps.inspect
|
109
|
+
|
110
|
+
# XXX this does not do cycles; we should really do cycles.
|
111
|
+
rank = @ranks.values_at(*deps).compact.max
|
112
|
+
@ranks[id] = rank.nil? ? 0 : rank + 1
|
113
|
+
|
114
|
+
# warn template.id
|
115
|
+
template
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return whether the group has a given key.
|
119
|
+
#
|
120
|
+
# @return [false, true] what I said.
|
121
|
+
#
|
122
|
+
def key? id
|
123
|
+
!!self[id]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Return the canonical template identifiers.
|
127
|
+
#
|
128
|
+
# @return [Array] the keys.
|
129
|
+
#
|
130
|
+
def keys ; @templates.keys; end
|
131
|
+
|
132
|
+
# Return the canonical identifier for the template.
|
133
|
+
#
|
134
|
+
# @param id [Object] the identifier, canonical or otherwise.
|
135
|
+
#
|
136
|
+
# @return [Object, nil] the canonical identifier, if found.
|
137
|
+
#
|
138
|
+
def canonical id
|
139
|
+
return id if @templates.key? id
|
140
|
+
@aliases[id].id if @aliases.key? id
|
141
|
+
end
|
142
|
+
|
143
|
+
# Return an array of arrays of templates sorted by rank. A higher
|
144
|
+
# rank means a parameter depends on one or more parameters with a
|
145
|
+
# lower rank.
|
146
|
+
#
|
147
|
+
# @return [Array<Hash{Object => Params::Registry::Template}>] the
|
148
|
+
# ranked parameter templates.
|
149
|
+
#
|
150
|
+
def ranked
|
151
|
+
# warn @ranks.inspect
|
152
|
+
|
153
|
+
out = Array.new((@ranks.values.max || -1) + 1) { {} }
|
154
|
+
|
155
|
+
# warn out.inspect
|
156
|
+
|
157
|
+
@templates.values.reject do |t|
|
158
|
+
# skip the complement as it's handled out of band
|
159
|
+
t.equal? registry.complement
|
160
|
+
end.each { |t| out[@ranks[t.id]][t.id] = t }
|
161
|
+
|
162
|
+
out
|
163
|
+
end
|
164
|
+
|
165
|
+
# Delete a template from the group.
|
166
|
+
#
|
167
|
+
# @param id [Object] the canonical identifier for the template, or an alias.
|
168
|
+
#
|
169
|
+
# @return [Params::Registry::Template, nil] the removed template,
|
170
|
+
# if there was one present to be removed.
|
171
|
+
#
|
172
|
+
def delete id
|
173
|
+
# first we have to find it
|
174
|
+
return unless template = self[id]
|
175
|
+
|
176
|
+
@templates.delete template.id
|
177
|
+
@ranks.delete template.id
|
178
|
+
@aliases.delete template.slug if template.slug
|
179
|
+
|
180
|
+
# XXX i feel like we should try to find other parameters that
|
181
|
+
# may have an alias that's the same as the one we just deleted
|
182
|
+
# and give (the first matching one) the now-empty slot, but
|
183
|
+
# that's not urgent so i'll leave it for now.
|
184
|
+
template.aliases.each do |a|
|
185
|
+
@aliases.delete a if template.equal? @aliases[a]
|
186
|
+
end
|
187
|
+
|
188
|
+
# if we are the main registry group we have to do extra stuff
|
189
|
+
if registry.templates.equal? self
|
190
|
+
registry.groups.each { |g| g.delete template.id }
|
191
|
+
end
|
192
|
+
|
193
|
+
# this leaves us with an unbound template which i gueessss we
|
194
|
+
# could reinsert?
|
195
|
+
template
|
196
|
+
end
|
197
|
+
|
198
|
+
# Return a suitable representation for debugging.
|
199
|
+
#
|
200
|
+
# @return [String] the object.
|
201
|
+
#
|
202
|
+
def inspect
|
203
|
+
"#<#{self.class}: #{id} {#{keys.join ', '}}>"
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
# The complement can be either an identifier or it can be a
|
211
|
+
# (partial) template spec with one additional `:id` member.
|
212
|
+
def coerce_complement complement
|
213
|
+
complement ||= :complement
|
214
|
+
# complement can be a name, or it can be a structure which is
|
215
|
+
# merged with the boilerplate below.
|
216
|
+
if complement.is_a? Hash
|
217
|
+
complement = Types::TemplateSpec[complement]
|
218
|
+
raise ArgumentError, 'Complement hash is missing :id' unless
|
219
|
+
complement.key? :id
|
220
|
+
spec = complement.except :id
|
221
|
+
complement = complement[:id]
|
222
|
+
else
|
223
|
+
spec = {}
|
224
|
+
end
|
225
|
+
|
226
|
+
# for the closures
|
227
|
+
ts = templates
|
228
|
+
|
229
|
+
# we always want these closures so we steamroll over whatever the
|
230
|
+
# user might have put in these slots
|
231
|
+
spec.merge!({
|
232
|
+
composite: Types::Set.constructor { |set|
|
233
|
+
# warn "heyooo #{set.inspect}"
|
234
|
+
raise Dry::Types::ConstraintError,
|
235
|
+
"#{complement} has values not found in templates" unless
|
236
|
+
set.all? { |t| ts.select { |_, x| x.complement? }.key? t }
|
237
|
+
Set[*set]
|
238
|
+
},
|
239
|
+
unwind: -> set {
|
240
|
+
# XXX do we want to sort this lexically or do we want it in
|
241
|
+
# the same order as the keys?
|
242
|
+
[set.to_a.map { |t| t = ts[t]; (t.slug || t.id).to_s }.sort, false]
|
243
|
+
}
|
244
|
+
})
|
245
|
+
|
246
|
+
[complement, spec]
|
247
|
+
end
|
248
|
+
|
249
|
+
public
|
250
|
+
|
251
|
+
# Initialize the registry. You will need to supply a set of specs
|
252
|
+
# that will become {::Params::Registry::Template} objects. You can
|
253
|
+
# also supply groups which you can use how you like.
|
254
|
+
#
|
255
|
+
# Parameters can be defined within groups or separately from them.
|
256
|
+
# This allows subsets of parameters to be easily hived off from the
|
257
|
+
# main {Params::Registry::Instance}.
|
258
|
+
#
|
259
|
+
# There is a special meta-parameter `:complement` which takes as its
|
260
|
+
# values the names of other parameters. This is intended to be used
|
261
|
+
# to signal that the parameters so named, assumed to be some kind of
|
262
|
+
# composite, like a {::Set} or {::Range}, are to be complemented or
|
263
|
+
# negated. The purpose of this is, for instance, if you want to
|
264
|
+
# express a parameter that is a set with many values, and rather
|
265
|
+
# than having to enumerate each of the values in something like a
|
266
|
+
# query string, you only have to enumerate the values you *don't*
|
267
|
+
# want in the set.
|
268
|
+
#
|
269
|
+
# @note The complement parameter is always set (last), and its
|
270
|
+
# identifier defaults, unsurprisingly, to `:complement`. This can
|
271
|
+
# be overridden by specifying an identifier you prefer. If you want
|
272
|
+
# a slug that is distinct from the canonical identifier, or if you
|
273
|
+
# want aliases, pass in a spec like this: `{ id:
|
274
|
+
# URI('https://my.schema/parameters#complement'), slug:
|
275
|
+
# :complement, aliases: %i[invert negate] }`
|
276
|
+
#
|
277
|
+
# @param templates [Hash] the hash of template specifications.
|
278
|
+
# @param groups [Hash, Array] the hash of groups.
|
279
|
+
# @param complement [Object, Hash] the identifier for the parameter
|
280
|
+
# for complementing composites, or otherwise a partial specification.
|
281
|
+
#
|
282
|
+
def initialize templates: nil, groups: nil, complement: nil
|
283
|
+
# initialize the object state with an empty default group
|
284
|
+
@groups = { nil => Group.new(self, nil) }
|
285
|
+
|
286
|
+
# coerce these guys
|
287
|
+
templates = Types::TemplateMap[templates]
|
288
|
+
groups = Types::GroupMap[groups]
|
289
|
+
|
290
|
+
# now load templates
|
291
|
+
templates.each { |id, spec| self.templates[id] = spec }
|
292
|
+
|
293
|
+
# now load groups
|
294
|
+
groups.each { |id, specs| self[id] = specs }
|
295
|
+
|
296
|
+
# now deal with complement
|
297
|
+
cid, cspec = coerce_complement complement
|
298
|
+
self.templates[cid] = cspec # XXX note leaky abstraction
|
299
|
+
# warn wtf.inspect
|
300
|
+
@complement = self.templates[cid]
|
301
|
+
end
|
302
|
+
|
303
|
+
# @!attribute [r] complement
|
304
|
+
# The `complement` template.
|
305
|
+
# @return [Params::Registry::Template]
|
306
|
+
attr_reader :complement
|
307
|
+
|
308
|
+
# Retrieve a group.
|
309
|
+
#
|
310
|
+
# @return [Params::Registry::Group] the group.
|
311
|
+
#
|
312
|
+
def [] id
|
313
|
+
@groups[id]
|
314
|
+
end
|
315
|
+
|
316
|
+
# Assign a group.
|
317
|
+
#
|
318
|
+
# @return [Params::Registry::Group] the new group.
|
319
|
+
#
|
320
|
+
def []= id, spec
|
321
|
+
# the null id is special; you can't assign to it
|
322
|
+
id = Types::NonNil[id]
|
323
|
+
|
324
|
+
@groups[id] = Group.new self, id, templates: spec
|
325
|
+
end
|
326
|
+
|
327
|
+
# Retrieve the names of the groups.
|
328
|
+
#
|
329
|
+
# @return [Array] the group names.
|
330
|
+
#
|
331
|
+
def keys
|
332
|
+
@groups.keys.reject(&:nil?)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Retrieve the groups themselves.
|
336
|
+
#
|
337
|
+
# @return [Array<Params::Registry::Group>] the groups.
|
338
|
+
#
|
339
|
+
def groups
|
340
|
+
@groups.values_at(*keys)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Retrieve the master template group.
|
344
|
+
#
|
345
|
+
# @return [Params::Registry::Group] the master group.
|
346
|
+
#
|
347
|
+
def templates
|
348
|
+
# XXX is this dumb? would it be better off as its own member?
|
349
|
+
@groups[nil]
|
350
|
+
end
|
351
|
+
|
352
|
+
# Process the parameters and return a {Params::Registry::Instance}.
|
353
|
+
#
|
354
|
+
# @param params
|
355
|
+
# [String, URI, Hash{#to_sym => Array}, Array<Array<(#to_sym, Object)>>]
|
356
|
+
# the parameter set, in a dizzying variety of inputs.
|
357
|
+
#
|
358
|
+
# @return [Params::Registry::Instance] the instance.
|
359
|
+
#
|
360
|
+
def process params
|
361
|
+
instance_class.new self, Types::Input[params]
|
362
|
+
end
|
363
|
+
|
364
|
+
# Refresh any stateful elements of the templates.
|
365
|
+
#
|
366
|
+
# @return [void]
|
367
|
+
#
|
368
|
+
def refresh!
|
369
|
+
templates.each { |t| t.refresh! }
|
370
|
+
nil
|
371
|
+
end
|
372
|
+
|
373
|
+
# @!group Quasi-static methods to override in subclasses
|
374
|
+
|
375
|
+
# The template class to use. Override this in a subclass if you want
|
376
|
+
# to use a custom one.
|
377
|
+
#
|
378
|
+
# @return [Class] the template class, {Params::Registry::Template}.
|
379
|
+
#
|
380
|
+
def template_class
|
381
|
+
Template
|
382
|
+
end
|
383
|
+
|
384
|
+
# The instance class to use. Override this in a subclass if you want
|
385
|
+
# to use a custom one.
|
386
|
+
#
|
387
|
+
# @return [Class] the instance class, {Params::Registry::Instance}.
|
388
|
+
#
|
389
|
+
def instance_class
|
390
|
+
Instance
|
391
|
+
end
|
392
|
+
|
393
|
+
# The group class to use. Override this in a subclass if you want
|
394
|
+
# to use a custom one.
|
395
|
+
#
|
396
|
+
# @return [Class] the group class, {Params::Registry::Group}.
|
397
|
+
#
|
398
|
+
def group_class
|
399
|
+
Group
|
400
|
+
end
|
401
|
+
|
402
|
+
# @!endgroup
|
403
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'params/registry'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/params/registry/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "params-registry"
|
7
|
+
spec.version = Params::Registry::VERSION
|
8
|
+
spec.authors = ["Dorian Taylor"]
|
9
|
+
spec.email = ["code@doriantaylor.com"]
|
10
|
+
|
11
|
+
spec.summary = "Params::Registry: a registry for URI query parameters"
|
12
|
+
spec.description =
|
13
|
+
File.open('README.md').read.split(/(\r?\n){2,}/).take(2).join("\n\n")
|
14
|
+
|
15
|
+
spec.homepage = "https://github.com/doriantaylor/rb-params-registry"
|
16
|
+
spec.license = "Apache-2.0"
|
17
|
+
spec.required_ruby_version = ">= 2.7"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
21
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) ||
|
28
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
# Uncomment to register a new dependency of your gem
|
36
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
37
|
+
spec.add_dependency 'dry-types', '~> 1.7'
|
38
|
+
|
39
|
+
# For more information and examples about making a new gem, check out our
|
40
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: params-registry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dorian Taylor
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-types
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
description: |+
|
28
|
+
# `Params::Registry`: A registry for named parameters
|
29
|
+
|
30
|
+
|
31
|
+
email:
|
32
|
+
- code@doriantaylor.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- ".rspec"
|
38
|
+
- ".yardopts"
|
39
|
+
- LICENSE
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- lib/params-registry.rb
|
43
|
+
- lib/params/registry.rb
|
44
|
+
- lib/params/registry/error.rb
|
45
|
+
- lib/params/registry/instance.rb
|
46
|
+
- lib/params/registry/template.rb
|
47
|
+
- lib/params/registry/types.rb
|
48
|
+
- lib/params/registry/version.rb
|
49
|
+
- params-registry.gemspec
|
50
|
+
- sig/params/registry.rbs
|
51
|
+
homepage: https://github.com/doriantaylor/rb-params-registry
|
52
|
+
licenses:
|
53
|
+
- Apache-2.0
|
54
|
+
metadata:
|
55
|
+
homepage_uri: https://github.com/doriantaylor/rb-params-registry
|
56
|
+
source_code_uri: https://github.com/doriantaylor/rb-params-registry
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '2.7'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.3.15
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: 'Params::Registry: a registry for URI query parameters'
|
76
|
+
test_files: []
|
77
|
+
...
|