params-registry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'types'
4
+ require_relative 'error'
5
+
6
+ # This class manages an individual parameter template.
7
+ class Params::Registry::Template
8
+
9
+ private
10
+
11
+ # this is dumb
12
+ Types = Params::Registry::Types
13
+
14
+ public
15
+
16
+ # Initialize the template object.
17
+ #
18
+ # @param registry [Params::Registry] A backreference to the
19
+ # parameter registry
20
+ # @param id [Object] The canonical, unique identifier for the
21
+ # parameter
22
+ # @param slug [#to_sym] A "friendly" symbol that will end up in
23
+ # the serialization
24
+ # @param aliases [Array<#to_sym>] Alternative nicknames for the
25
+ # parameter
26
+ # @param type [Dry::Types::Type, Symbol, Proc] An "atomic" type
27
+ # for single values
28
+ # @param composite [Dry::Types::Type, Symbol, Proc] A composite
29
+ # type into which multiple values are loaded
30
+ # @param format [String, Proc, nil] A format string or function
31
+ # @param depends [Array] Parameters that this one depends on
32
+ # @param conflicts [Array] Parameters that conflict with this one
33
+ # @param consumes [Array] Parameters that can be given in lieu of
34
+ # this one, that will be composed into this one. Parameters this
35
+ # one `consumes` implies `depends` _and_ `conflicts`.
36
+ # @param preproc [Proc, nil] A preprocessing function that is fed
37
+ # parameters from `consumes` and `depends` to generate this
38
+ # parameter
39
+ # @param min [Integer, nil] Minimum cardinality
40
+ # @param max [Integer, nil] Maximum cardinality
41
+ # @param shift [false, true] When given more than `max` values, do
42
+ # we take the ones we want from the back or from the front
43
+ # @param empty [false, true, nil] whether to treat an empty value
44
+ # as nil, the empty string, or discard it
45
+ # @param default [Object, nil] A default value
46
+ # @param universe [Proc] For {::Set} or {::Range} composite types and
47
+ # derivatives, a function that returns the universal set or range
48
+ # @param complement [Proc] For {::Set} or {::Range} composite types, a
49
+ # function that will return the complement of the set or range
50
+ # @param unwind [Proc] A function that takes the composite type
51
+ # and turns it into an {::Array} of atomic values
52
+ # @param reverse [false, true] For {::Range} composite types, a flag
53
+ # that indicates whether the values should be interpreted and/or
54
+ # serialized in reverse order. Also governs the serialization of
55
+ # {::Set} composites.
56
+ #
57
+ def initialize registry, id, slug: nil, type: Types::NormalizedString,
58
+ composite: nil, format: nil, aliases: nil, depends: nil, conflicts: nil,
59
+ consumes: nil, preproc: nil, min: 0, max: nil, shift: false,
60
+ empty: false, default: nil, universe: nil, complement: nil,
61
+ unwind: nil, reverse: false
62
+
63
+ @registry = Types::Registry[registry]
64
+ @id = Types::NonNil[id]
65
+ @slug = Types::Symbol[slug] if slug
66
+ @type = Types[type]
67
+ @composite = Types[composite] if composite
68
+ @format = (Types::Proc | Types::String)[format] if format
69
+ @aliases = Types::Array[aliases]
70
+ @depends = Types::Array[depends]
71
+ @conflicts = Types::Array[conflicts]
72
+ @consumes = Types::Array[consumes]
73
+ @preproc = Types::Proc[preproc] if preproc
74
+ @min = Types::NonNegativeInteger[min || 0]
75
+ @max = Types::PositiveInteger.optional[max]
76
+ @shift = Types::Bool[shift]
77
+ @empty = Types::Bool[empty]
78
+ @default = Types::Nominal::Any[default]
79
+ @unifunc = Types::Proc[universe] if universe
80
+ @complement = Types::Proc[complement] if complement
81
+ @unwind = Types::Proc[unwind] if unwind
82
+ @reverse = Types::Bool[reverse]
83
+
84
+ end
85
+
86
+ # @!attribute [r] registry
87
+ # @return [Params::Registry] a backreference to the registry.
88
+ #
89
+ # @!attribute [r] id
90
+ # @return [Object] the canonical identifier for the parameter.
91
+ #
92
+ # @!attribute [r] slug
93
+ # @return [Symbol, nil] the primary nickname for the parameter, if
94
+ # different from the `id`.
95
+ #
96
+ # @!attribute [r] type
97
+ # @return [Dry::Types::Type] the type for individual parameter values.
98
+ #
99
+ # @!attribute [r] composite
100
+ # @return [Dry::Types::Type, nil] the type for composite values.
101
+ #
102
+ # @!attribute [r] aliases
103
+ # @return [Array<Symbol>] any aliases for this parameter.
104
+ #
105
+ # @!attribute [r] preproc
106
+ # @return [Proc] a procedure to run over `consume`d parameters.
107
+ #
108
+ # @!attribute [r] min
109
+ # @return [Integer] minimum cardinality for the parameter's values.
110
+ #
111
+ # @!attribute [r] max
112
+ # @return [Integer, nil] maximum cardinality for the parameter's values.
113
+ #
114
+ # @!attribute [r] default
115
+ # @return [Object, nil] a default value for the parameter.
116
+ #
117
+ # @!attribute [r] unwind
118
+ # A function that will take a composite object
119
+ # and turn it into an array of strings for serialization.
120
+ # @return [Proc, nil]
121
+
122
+ attr_reader :registry, :id, :slug, :type, :composite, :aliases,
123
+ :preproc, :min, :max, :default, :unwind
124
+
125
+ # @!attribute [r] depends
126
+ # Any parameters this one depends on.
127
+ #
128
+ # @return [Array]
129
+ #
130
+ def depends
131
+ out = (@depends | (@preproc ? @consumes : [])).map do |t|
132
+ registry.templates.canonical t
133
+ end
134
+
135
+ raise Params::Registry::Error,
136
+ "Malformed dependency declaration on #{t.id}" if out.any?(&:nil?)
137
+
138
+ out
139
+ end
140
+
141
+ # @!attribute [r] conflicts
142
+ # Any parameters this one conflicts with.
143
+ #
144
+ # @return [Array]
145
+ #
146
+ def conflicts
147
+ out = (@conflicts | (@preproc ? @consumes : [])).map do |t|
148
+ registry.templates.canonical t
149
+ end
150
+
151
+ raise Params::Registry::Error,
152
+ "Malformed conflict declaration on #{t.id}" if out.any?(&:nil?)
153
+
154
+ out
155
+ end
156
+
157
+ # @!attribute [r] preproc?
158
+ # Whether there is a preprocessor function.
159
+ #
160
+ # @return [Boolean]
161
+ #
162
+ def preproc? ; !!@preproc ; end
163
+
164
+ # @!attribute [r] consumes
165
+ # Any parameters this one consumes (implies `depends` + `conflicts`).
166
+ #
167
+ # @return [Array]
168
+ #
169
+ def consumes
170
+ out = @consumes.map { |t| registry.templates.canonical t }
171
+
172
+ raise Params::Registry::Error,
173
+ "Malformed consumes declaration on #{t.id}" if out.any?(&:nil?)
174
+
175
+ out
176
+ end
177
+
178
+ # @!attribute [r] universe
179
+ # The universal composite object (e.g. set or range) from which
180
+ # valid values are drawn.
181
+ # @return [Object, nil]
182
+ def universe
183
+ refresh! unless @universe
184
+ @universe
185
+ end
186
+
187
+ # @!attribute [r] shift?
188
+ # Whether to shift values more than `max` cardinality off the front.
189
+ #
190
+ # @return [Boolean]
191
+ #
192
+ def shift? ; !!@shift; end
193
+
194
+ # @!attribute [r] empty?
195
+ # Whether to accept empty values.
196
+ #
197
+ # @return [Boolean]
198
+ #
199
+ def empty? ; !!@empty; end
200
+
201
+ # @!attribute [r] reverse?
202
+ # Whether to interpret composite values as reversed.
203
+ #
204
+ # @return [Boolean]
205
+ #
206
+ def reverse? ; !!@reverse; end
207
+
208
+ # @!attribute [r] complement?
209
+ # Whether this (composite) parameter can be complemented or inverted.
210
+ #
211
+ # @return [Boolean]
212
+ #
213
+ def complement? ; !!@complement; end
214
+
215
+ # Preprocess a parameter value against itself and/or `consume`d values.
216
+ #
217
+ # @param myself [Array] raw values for the parameter itself.
218
+ # @param others [Array] *processed* values for the consumed parameters.
219
+ #
220
+ # @return [Array] pseudo-raw, preprocessed values for the parameter.
221
+ #
222
+ def preproc myself, others
223
+ begin
224
+ # run preproc in the context of the template
225
+ out = instance_exec myself, others, &@preproc
226
+ out = [out] unless out.is_a? Array
227
+ rescue Dry::Types::CoercionError => e
228
+ # rethrow a better error
229
+ raise Params::Registry::Error.new(
230
+ "Preprocessor failed on #{template.id} with #{}",
231
+ context: self, value: e)
232
+ end
233
+
234
+ out
235
+ end
236
+
237
+ # Format an individual atomic value.
238
+ #
239
+ # @param scalar [Object] the scalar/atomic value.
240
+ #
241
+ # @return [String] serialized to a string.
242
+ #
243
+ def format scalar
244
+ return scalar.to_s unless @format
245
+
246
+ if @format.is_a? Proc
247
+ instance_exec scalar, &@format
248
+ else
249
+ @format.to_s % scalar
250
+ end
251
+ end
252
+
253
+ # Return the complement of the composite value for the parameter.
254
+ #
255
+ # @param value [Object] the composite object to complement.
256
+ #
257
+ # @return [Object, nil] the complementary object, if a complement is defined.
258
+ #
259
+ def complement value
260
+ return unless @complement
261
+ begin
262
+ instance_exec value, &@complement
263
+ rescue e
264
+ raise Params::Registry::Error::Empirical.new(
265
+ "Complement function failed: #{e.message}",
266
+ context: self, value: value)
267
+ end if @complement
268
+ end
269
+
270
+ # Validate a list of individual parameter values and (if one is present)
271
+ # construct a `composite` value.
272
+ #
273
+ # @param values [Array] the values given for the parameter.
274
+ #
275
+ # @return [Object, Array] the processed value(s).
276
+ #
277
+ def process *values
278
+ out = []
279
+
280
+ values.each do |v|
281
+ # skip over nil/empty values unless we can be empty
282
+ if v.nil? or v.to_s.empty?
283
+ next unless empty?
284
+ v = nil
285
+ end
286
+
287
+ if v
288
+ begin
289
+ tmp = type[v] # this either crashes or it doesn't
290
+ v = tmp # in which case v is only assigned if successful
291
+ rescue Dry::Types::CoercionError => e
292
+ raise Params::Registry::Error::Syntax.new e.message,
293
+ context: self, value: v
294
+ end
295
+ end
296
+
297
+ out << v
298
+ end
299
+
300
+ # now we deal with cardinality
301
+ raise Params::Registry::Error::Cardinality.new(
302
+ "Need #{min} values and there are only #{out.length} values") if
303
+ out.length < min
304
+
305
+ # warn "hurr #{out.inspect}, #{max}"
306
+
307
+ if max
308
+ # return if it's supposed to be a scalar value
309
+ return out.first if max == 1
310
+ # cut the values to length from either the front or back
311
+ out.slice!((shift? ? -max : 0), max) if out.length > max
312
+ end
313
+
314
+ composite ? composite[out] : out
315
+ end
316
+
317
+ # Applies `unwind` to `value` to get an array, then `format` over
318
+ # each of the elements to get strings. If `scalar` is true, it
319
+ # will also return the flag from `unwind` indicating whether or
320
+ # not the `complement` parameter should be set.
321
+ #
322
+ # This method is called by {Params::Registry::Instance#to_s} and
323
+ # others to produce content which is amenable to serialization. As
324
+ # what happens there, the content of `rest` should be the values
325
+ # of the parameters specified in `depends`.
326
+ #
327
+ # @param value [Object, Array<Object>] The parameter value(s).
328
+ # @param rest [Array<Object>] The rest of the parameter values.
329
+ # @param with_complement_flag [false, true] Whether to return the
330
+ # `complement` flag in addition to the unwound values.
331
+ #
332
+ # @return [Array<String>, Array<(Array<String>, false)>,
333
+ # Array<(Array<String>, true)>, nil] the unwound value(s), plus
334
+ # optionally the `complement` flag, or otherwise `nil`.
335
+ #
336
+ def unprocess value, *rest, with_complement_flag: false
337
+ # take care of empty properly
338
+ if value.nil?
339
+ if empty?
340
+ return [''] if max == 1
341
+ return [] if max.nil? or max > 1
342
+ end
343
+
344
+ # i guess this is nil?
345
+ return
346
+ end
347
+
348
+ # complement flag
349
+ comp = false
350
+ begin
351
+ tmp, comp = instance_exec value, *rest, &@unwind
352
+ value = tmp
353
+ rescue Exception, e
354
+ raise Params::Registry::Error::Empirical.new(
355
+ "Cannot unprocess value #{value} for parameter #{id}: #{e.message}",
356
+ context: self, value: value)
357
+ end if @unwind
358
+
359
+ # ensure this thing is an array
360
+ value = [value] unless value.is_a? Array
361
+
362
+ # ensure the values are correctly formatted
363
+ value.map! { |v| v.nil? ? '' : self.format(v) }
364
+
365
+ # throw in the complement flag
366
+ return value, comp if with_complement_flag
367
+
368
+ value
369
+ end
370
+
371
+ # Refreshes stateful information like the universal set, if present.
372
+ #
373
+ # @return [void]
374
+ #
375
+ def refresh!
376
+ if @unifunc
377
+ # do we want to call or do we want to instance_exec?
378
+ univ = @unifunc.call
379
+
380
+ univ = @composite[univ] if @composite
381
+
382
+ @universe = univ
383
+ end
384
+
385
+ nil
386
+ end
387
+
388
+ # Return a suitable representation for debugging.
389
+ #
390
+ # @return [String] the object.
391
+ #
392
+ def inspect
393
+ c = self.class
394
+ i = id.inspect
395
+ t = '%s%s' % [type, composite ? ", #{composite}]" : '']
396
+
397
+ "#<#{c} #{i} (#{t})>"
398
+ end
399
+
400
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+
5
+ require 'dry-types' # let's try to use this and not hate it
6
+ require 'set' # for some reason Set is not in the kernel but Range is
7
+ require 'date' # includes DateTime
8
+ require 'time' # ruby has Time and DateTime to be confusing
9
+ require 'uri'
10
+
11
+ # All the type coercions used in {Params::Registry}.
12
+ module Params::Registry::Types
13
+ include Dry.Types(default: :coercible)
14
+
15
+ # Syntactic sugar for retrieving types in the library.
16
+ #
17
+ # @param const [#to_sym] The type (name).
18
+ #
19
+ # @return [Dry::Types::Type] The type instance.
20
+ #
21
+ def self.[] const
22
+ return const if const.is_a? Dry::Types::Type
23
+ begin
24
+ const_get const.to_s.to_sym
25
+ rescue NameError
26
+ raise ArgumentError, "No type named #{const}"
27
+ end
28
+ end
29
+
30
+ # Can be anything, as long as it isn't `nil`.
31
+ NonNil = Strict::Any.constrained not_eql: nil
32
+
33
+ # Gotta have a coercible boolean (which doesn't come stock for some reason)
34
+ Bool = Nominal::Bool.constructor do |x|
35
+ case x.to_s.strip
36
+ when /\A(1|true|on|yes)\Z/i then true
37
+ when /\A(0|false|off|no|)\Z/i then false
38
+ else
39
+ raise Dry::Types::CoercionError, "#{x} can't be coerced to true or false"
40
+ end
41
+ end
42
+
43
+ # For some reason there isn't a stock `Proc` type.
44
+ Proc = self.Instance(::Proc)
45
+
46
+ # @!group A bunch of integer types
47
+
48
+ # The problem with Kernel.Integer is that if a string representing
49
+ # a number begins with a zero it's treated as octal, so we have to
50
+ # compensate for that.
51
+ Base10Integer = Nominal::Integer.constructor do |i|
52
+ i.is_a?(::Numeric) ? i.to_i : ::Kernel.Integer(i.to_s, 10)
53
+ end
54
+
55
+ # `xsd:nonPositiveInteger`
56
+ NonPositiveInteger = Base10Integer.constrained lteq: 0
57
+ # `xsd:nonNegativeInteger`
58
+ NonNegativeInteger = Base10Integer.constrained gteq: 0
59
+ # `xsd:positiveInteger`
60
+ PositiveInteger = Base10Integer.constrained gt: 0
61
+ # `xsd:negativeInteger`
62
+ NegativeInteger = Base10Integer.constrained lt: 0
63
+
64
+ # @!group Stringy stuff, à la XSD plus some others
65
+
66
+ # This is `xsd:normalizedString`.
67
+ NormalizedString = Nominal::String.constructor do |s|
68
+ s.to_s.gsub(/[\t\r\n]/, ' ')
69
+ end
70
+
71
+ # This is `xsd:token`.
72
+ Token = NormalizedString.constructor { |s| s.tr_s(' ', ' ').strip }
73
+
74
+ # Coerce an `xsd:token` into a {::Symbol}.
75
+ Symbol = Token.constructor { |t| t.to_sym }
76
+
77
+ # Coerce an `xsd:token` into a symbol with all lower-case letters.
78
+ LCSymbol = Token.constructor { |t| t.downcase.to_sym }
79
+
80
+ # Do the same but with upper-case letters.
81
+ UCSymbol = Token.constructor { |t| t.upcase.to_sym }
82
+
83
+ # Create a symbol with all whitespace and underscores turned to hyphens.
84
+ HyphenSymbol = Token.constructor { |t| t.tr_s(' _', ?-).to_sym }
85
+
86
+ # Do the same but with all lower-case letters.
87
+ LCHyphenSymbol = HyphenSymbol.constructor { |s| s.to_s.downcase.to_sym }
88
+
89
+ # Do the same but with all upper-case letters.
90
+ UCHyphenSymbol = HyphenSymbol.constructor { |s| s.to_s.upcase.to_sym }
91
+
92
+ # Create a symbol with all whitespace and hyphens turned to underscores.
93
+ UnderscoreSymbol = Token.constructor { |t| t.tr_s(' -', ?_).to_sym }
94
+
95
+ # Do the same but with all lower-case letters.
96
+ LCUnderscoreSymbol = UnderscoreSymbol.constructor do |s|
97
+ s.to_s.downcase.to_sym
98
+ end
99
+
100
+ # Do the same but with all upper-case letters.
101
+ UCUnderscoreSymbol = UnderscoreSymbol.constructor do |s|
102
+ s.to_s.upcase.to_sym
103
+ end
104
+
105
+ # @!group Dates & Times
106
+
107
+ # Ye olde {::Date}
108
+ Date = self.Constructor(::Date) do |x|
109
+ case x
110
+ when ::Array then ::Date.new(*x.take(3))
111
+ else ::Date.parse x
112
+ end
113
+ end
114
+
115
+ # And {::DateTime}
116
+ DateTime = self.Constructor(::DateTime) do |x|
117
+ ::DateTime.parse x
118
+ end
119
+
120
+ # Aaand {::Time}
121
+ Time = self.Constructor(::Time) do |x|
122
+ case x
123
+ when ::Array then ::Time.new(*x)
124
+ when (Base10Integer[x] rescue nil) then ::Time.at(Base10Integer[x])
125
+ else ::Time.parse x
126
+ end
127
+ end
128
+
129
+ # @!group Composite types not already defined
130
+
131
+ # {::Set}
132
+ Set = self.Constructor(::Set) { |x| ::Set[*x] }
133
+
134
+ # {::Range}
135
+ Range = self.Constructor(::Range) { |x| ::Range.new(*x.take(2)) }
136
+
137
+ # The registry itself
138
+ Registry = self.Instance(::Params::Registry)
139
+
140
+ # Templates
141
+
142
+ TemplateSpec = Hash.map(Symbol, Strict::Any)
143
+
144
+ TemplateMap = Hash|Hash.map(NonNil, TemplateSpec)
145
+
146
+ # Groups
147
+ GroupMap = Hash|Hash.map(NonNil, Array|TemplateMap)
148
+
149
+ Input = self.Constructor(::Hash) do |input|
150
+ input = input.query.to_s if input.is_a? ::URI
151
+ input = ::URI.decode_www_form input if input.is_a? ::String
152
+
153
+ case input
154
+ when ::Hash then Hash.map(Symbol, Array.of(String))[input]
155
+ when ::Array
156
+ input.reduce({}) do |out, pair|
157
+ k, *v = Strict::Array.constrained(min_size: 2)[pair]
158
+ (out[k.to_sym] ||= []).push(*v)
159
+ out
160
+ end
161
+ else
162
+ raise Dry::Types::CoercionError, "not sure what to do with #{input}"
163
+ end
164
+ end
165
+
166
+ # @!endgroup
167
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Params
4
+ class Registry
5
+ # The module version
6
+ VERSION = "0.1.0"
7
+ end
8
+ end