params-registry 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
...
|