plumb 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,12 +12,17 @@ module Plumb
12
12
  DEFAULT = 'default'
13
13
  ANY_OF = 'anyOf'
14
14
  ALL_OF = 'allOf'
15
+ NOT = 'not'
15
16
  ENUM = 'enum'
16
17
  CONST = 'const'
17
18
  ITEMS = 'items'
18
19
  PATTERN = 'pattern'
19
20
  MINIMUM = 'minimum'
20
21
  MAXIMUM = 'maximum'
22
+ MIN_ITEMS = 'minItems'
23
+ MAX_ITEMS = 'maxItems'
24
+ MIN_LENGTH = 'minLength'
25
+ MAX_LENGTH = 'maxLength'
21
26
 
22
27
  def self.call(node)
23
28
  {
@@ -78,7 +83,7 @@ module Plumb
78
83
  end
79
84
 
80
85
  on(:not) do |node, props|
81
- props.merge('not' => visit(node.step))
86
+ props.merge(NOT => visit(node.step))
82
87
  end
83
88
 
84
89
  on(:value) do |node, props|
@@ -113,14 +118,52 @@ module Plumb
113
118
  visit(node.value, props)
114
119
  end
115
120
 
116
- on(:rules) do |node, props|
117
- node.rules.reduce(props) do |acc, rule|
118
- acc.merge(visit(rule))
121
+ on(:policy) do |node, props|
122
+ props = visit(node.step, props)
123
+ method_name = :"visit_#{node.policy_name}_policy"
124
+ if respond_to?(method_name)
125
+ send(method_name, node, props)
126
+ else
127
+ props
119
128
  end
120
129
  end
121
130
 
122
- on(:rule_included_in) do |node, props|
123
- props.merge(ENUM => node.arg_value)
131
+ on(:options_policy) do |node, props|
132
+ props.merge(ENUM => node.arg)
133
+ end
134
+
135
+ on(:size_policy) do |node, props|
136
+ opts = {}
137
+ case props[TYPE]
138
+ when 'array'
139
+ case node.arg
140
+ when Range
141
+ opts[MIN_ITEMS] = node.arg.min if node.arg.begin
142
+ opts[MAX_ITEMS] = node.arg.max if node.arg.end
143
+ when Numeric
144
+ opts[MIN_ITEMS] = node.arg
145
+ opts[MAX_ITEMS] = node.arg
146
+ end
147
+ when 'string'
148
+ case node.arg
149
+ when Range
150
+ opts[MIN_LENGTH] = node.arg.min if node.arg.begin
151
+ opts[MAX_LENGTH] = node.arg.max if node.arg.end
152
+ when Numeric
153
+ opts[MIN_LENGTH] = node.arg
154
+ opts[MAX_LENGTH] = node.arg
155
+ end
156
+ end
157
+
158
+ props.merge(opts)
159
+ end
160
+
161
+ on(:excluded_from_policy) do |node, props|
162
+ props.merge(NOT => { ENUM => node.arg })
163
+ end
164
+
165
+ on(Proc) do |_node, props|
166
+ props
124
167
  end
125
168
 
126
169
  on(:match) do |node, props|
@@ -8,11 +8,12 @@ module Plumb
8
8
 
9
9
  attr_reader :matcher
10
10
 
11
- def initialize(matcher = Undefined, error: nil)
11
+ def initialize(matcher = Undefined, error: nil, label: nil)
12
12
  raise TypeError 'matcher must respond to #===' unless matcher.respond_to?(:===)
13
13
 
14
14
  @matcher = matcher
15
15
  @error = error.nil? ? build_error(matcher) : (error % matcher)
16
+ @label = matcher.is_a?(Class) ? matcher.inspect : "Match(#{label || @matcher.inspect})"
16
17
  freeze
17
18
  end
18
19
 
@@ -22,9 +23,7 @@ module Plumb
22
23
 
23
24
  private
24
25
 
25
- def _inspect
26
- @matcher.inspect
27
- end
26
+ def _inspect = @label
28
27
 
29
28
  def build_error(matcher)
30
29
  case matcher
@@ -77,7 +77,12 @@ module Plumb
77
77
  end
78
78
 
79
79
  on(:static) do |node, props|
80
- props.merge(static: node.value)
80
+ type = node.value.is_a?(Class) ? node.value : node.value.class
81
+ props.merge(static: node.value, type:)
82
+ end
83
+
84
+ on(:policy) do |node, props|
85
+ visit(node.step, props).merge(node.policy_name => node.arg)
81
86
  end
82
87
 
83
88
  on(:rules) do |node, props|
@@ -117,5 +122,9 @@ module Plumb
117
122
  on(:tagged_hash) do |_node, props|
118
123
  props.merge(type: Hash)
119
124
  end
125
+
126
+ on(Proc) do |_node, props|
127
+ props
128
+ end
120
129
  end
121
130
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumb/policies'
4
+
5
+ module Plumb
6
+ # A policy registry for Plumb
7
+ # It holds and gets registered policies.
8
+ # Policies are callable objects that act as factories for type compositions.
9
+ class Policies
10
+ UnknownPolicyError = Class.new(StandardError)
11
+ MethodAlreadyDefinedError = Class.new(StandardError)
12
+
13
+ def initialize
14
+ @policies = {}
15
+ end
16
+
17
+ # Register a policy for all or specific outpyt types.
18
+ # Example for a policy that works for all types:
19
+ # #register(Object, :my_policy, ->(node, arg) { ... })
20
+ # Example for a policy that works for a specific type:
21
+ # #register(String, :my_policy, ->(node, arg) { ... })
22
+ # Example for a policy that works for a specific interface:
23
+ # #register(:size, :my_policy, ->(node, arg) { ... })
24
+ #
25
+ # The policy callable takes the step it is applied to, a policy argument (if any) and a policy block (if any).
26
+ # Example for a policy #default(default_value = Undefined) { 'some-default-value' }
27
+ # policy = proc do |type, default_value = Undefined, &block|
28
+ # type | (Plumb::Types::Undefined >> Plumb::Types::Static[default_value])
29
+ # end
30
+ #
31
+ # @param for_type [Class, Symbol] the type the policy is for.
32
+ # @param name [Symbol] the name of the policy.
33
+ # @param policy [Proc] the policy to register.
34
+ def register(for_type, name, policy)
35
+ @policies[name] ||= {}
36
+ @policies[name][for_type] = policy
37
+ end
38
+
39
+ # Get a policy for a given type.
40
+ # @param types [Array<Class>] the types
41
+ # @param name [Symbol] the policy name
42
+ # @return [#call] the policy callable
43
+ # @raise [UnknownPolicyError] if the policy is not registered for the given types
44
+ def get(types, name)
45
+ if (pol = resolve_shared_policy(types, name))
46
+ pol
47
+ elsif (pol = @policies.dig(name, Object))
48
+ raise UnknownPolicyError, "Unknown policy #{name} for #{types.inspect}" unless pol
49
+
50
+ pol
51
+ else
52
+ raise UnknownPolicyError, "Unknown or incompatible policy #{name} for #{types.inspect}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def resolve_shared_policy(types, name)
59
+ pols = types.map do |type|
60
+ resolve_policy(type, name)
61
+ end.uniq
62
+ pols.size == 1 ? pols.first : nil
63
+ end
64
+
65
+ def resolve_policy(type, name)
66
+ policies = @policies[name]
67
+ return nil unless policies
68
+
69
+ # { Object => policy1, String => policy2, size: policy3 }
70
+ #
71
+ policies.find do |for_type, _pol|
72
+ case for_type
73
+ when Symbol # :size
74
+ type.instance_methods.include?(for_type)
75
+ when Class # String
76
+ for_type == type
77
+ end
78
+ end&.last
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumb/steppable'
4
+
5
+ module Plumb
6
+ # Wrap a policy composition ("step") in a Policy object.
7
+ # So that visitors such as JSONSchema and Metadata visitors
8
+ # can define dedicated handlers for policies, if they need to.
9
+ class Policy
10
+ include Steppable
11
+
12
+ attr_reader :policy_name, :arg, :step
13
+
14
+ # @param policy_name [Symbol]
15
+ # @param arg [Object, nil] the argument to the policy, if any.
16
+ # @param step [Step] the step composition wrapped by this policy.
17
+ def initialize(policy_name, arg, step)
18
+ @policy_name = policy_name
19
+ @arg = arg
20
+ @step = step
21
+ freeze
22
+ end
23
+
24
+ # The standard Step interface.
25
+ # @param result [Result::Valid]
26
+ # @return [Result::Valid, Result::Invalid]
27
+ def call(result) = @step.call(result)
28
+
29
+ private def _inspect = @step.inspect
30
+ end
31
+ end
data/lib/plumb/schema.rb CHANGED
@@ -172,8 +172,8 @@ module Plumb
172
172
  self
173
173
  end
174
174
 
175
- def rule(...)
176
- @_type = @_type.rule(...)
175
+ def policy(...)
176
+ @_type = @_type.policy(...)
177
177
  self
178
178
  end
179
179
 
@@ -10,6 +10,7 @@ module Plumb
10
10
 
11
11
  def to_s = inspect
12
12
  def node_name = :undefined
13
+ def empty? = true
13
14
  end
14
15
 
15
16
  TypeError = Class.new(::TypeError)
@@ -111,7 +112,7 @@ module Plumb
111
112
  end
112
113
 
113
114
  def check(errors = 'did not pass the check', &block)
114
- self >> MatchClass.new(block, error: errors)
115
+ self >> MatchClass.new(block, error: errors, label: errors)
115
116
  end
116
117
 
117
118
  def meta(data = {})
@@ -136,22 +137,6 @@ module Plumb
136
137
 
137
138
  def [](val) = match(val)
138
139
 
139
- DefaultProc = proc do |callable|
140
- proc do |result|
141
- result.valid(callable.call)
142
- end
143
- end
144
-
145
- def default(val = Undefined, &block)
146
- val_type = if val == Undefined
147
- DefaultProc.call(block)
148
- else
149
- Types::Static[val]
150
- end
151
-
152
- self | (Types::Undefined >> val_type)
153
- end
154
-
155
140
  class Node
156
141
  include Steppable
157
142
 
@@ -171,29 +156,28 @@ module Plumb
171
156
  Node.new(node_name, self, metadata)
172
157
  end
173
158
 
174
- def nullable
175
- Types::Nil | self
176
- end
177
-
178
- def present
179
- Types::Present >> self
180
- end
181
-
182
- def options(opts = [])
183
- rule(included_in: opts)
184
- end
159
+ # Register a policy for this step.
160
+ # Mode 1.a: #policy(:name, arg) a single policy with an argument
161
+ # Mode 1.b: #policy(:name) a single policy without an argument
162
+ # Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
163
+ # The latter mode will be expanded to multiple #policy calls.
164
+ # @return [Step]
165
+ def policy(*args, &blk)
166
+ case args
167
+ in [::Symbol => name, *rest] # #policy(:name, arg)
168
+ types = Array(metadata[:type]).uniq
185
169
 
186
- def rule(*args)
187
- specs = case args
188
- in [::Symbol => rule_name, value]
189
- { rule_name => value }
190
- in [::Hash => rules]
191
- rules
192
- else
193
- raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
194
- end
170
+ bargs = [self]
171
+ bargs << rest.first if rest.any?
172
+ block = Plumb.policies.get(types, name)
173
+ pol = block.call(*bargs, &blk)
195
174
 
196
- self >> Rules.new(specs, metadata[:type])
175
+ Policy.new(name, rest.first, pol)
176
+ in [::Hash => opts] # #policy(p1: value, p2: value)
177
+ opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
178
+ else
179
+ raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
180
+ end
197
181
  end
198
182
 
199
183
  def ===(other)
@@ -217,7 +201,7 @@ module Plumb
217
201
  inspect
218
202
  end
219
203
 
220
- # Build a step that will invoke onr or more methods on the value.
204
+ # Build a step that will invoke one or more methods on the value.
221
205
  # Ex 1: Types::String.invoke(:downcase)
222
206
  # Ex 2: Types::Array.invoke(:[], 1)
223
207
  # Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
@@ -240,5 +224,6 @@ end
240
224
 
241
225
  require 'plumb/deferred'
242
226
  require 'plumb/transform'
227
+ require 'plumb/policy'
243
228
  require 'plumb/build'
244
229
  require 'plumb/metadata'
data/lib/plumb/types.rb CHANGED
@@ -3,25 +3,127 @@
3
3
  require 'bigdecimal'
4
4
 
5
5
  module Plumb
6
- Rules.define :included_in, 'elements must be included in %<value>s', expects: ::Array do |result, opts|
7
- result.value.all? { |v| opts.include?(v) }
6
+ # Define core policies
7
+ # Allowed options for an array type.
8
+ # It validates that each element is in the options array.
9
+ # Usage:
10
+ # type = Types::Array.options(['a', 'b'])
11
+ policy :options, helper: true, for_type: ::Array do |type, opts|
12
+ type.check("must be included in #{opts.inspect}") do |v|
13
+ v.all? { |val| opts.include?(val) }
14
+ end
15
+ end
16
+
17
+ # Generic options policy for all other types.
18
+ # Usage:
19
+ # type = Types::String.options(['a', 'b'])
20
+ policy :options do |type, opts|
21
+ type.check("must be included in #{opts.inspect}") do |v|
22
+ opts.include?(v)
23
+ end
24
+ end
25
+
26
+ # Validate that array elements are NOT in the options array.
27
+ # Usage:
28
+ # type = Types::Array.policy(excluded_from: ['a', 'b'])
29
+ policy :excluded_from, for_type: ::Array do |type, opts|
30
+ type.check("must not be included in #{opts.inspect}") do |v|
31
+ v.none? { |val| opts.include?(val) }
32
+ end
33
+ end
34
+
35
+ # Usage:
36
+ # type = Types::String.policy(excluded_from: ['a', 'b'])
37
+ policy :excluded_from do |type, opts|
38
+ type.check("must not be included in #{opts.inspect}") do |v|
39
+ !opts.include?(v)
40
+ end
41
+ end
42
+
43
+ # Validate #size against a number or any object that responds to #===.
44
+ # This works with any type that repsonds to #size.
45
+ # Usage:
46
+ # type = Types::String.policy(size: 10)
47
+ # type = Types::Integer.policy(size: 1..10)
48
+ # type = Types::Array.policy(size: 1..)
49
+ # type = Types::Any[Set].policy(size: 1..)
50
+ policy :size, for_type: :size do |type, size|
51
+ type.check("must be of size #{size}") { |v| size === v.size }
8
52
  end
9
- Rules.define :included_in, 'must be included in %<value>s' do |result, opts|
10
- opts.include? result.value
53
+
54
+ # Validate that an object is not #empty? nor #nil?
55
+ # Usage:
56
+ # Types::String.present
57
+ # Types::Array.present
58
+ policy :present, helper: true do |type, *_args|
59
+ type.check('must be present') do |v|
60
+ if v.respond_to?(:empty?)
61
+ !v.empty?
62
+ else
63
+ !v.nil?
64
+ end
65
+ end
11
66
  end
12
- Rules.define :excluded_from, 'elements must not be included in %<value>s', expects: ::Array do |result, value|
13
- result.value.all? { |v| !value.include?(v) }
67
+
68
+ # Allow nil values for a type.
69
+ # Usage:
70
+ # nullable_int = Types::Integer.nullable
71
+ # nullable_int.parse(nil) # => nil
72
+ # nullable_int.parse(10) # => 10
73
+ # nullable_int.parse('nope') # => error: not an Integer
74
+ policy :nullable, helper: true do |type, *_args|
75
+ Types::Nil | type
14
76
  end
15
- Rules.define :excluded_from, 'must not be included in %<value>s' do |result, value|
16
- !value.include?(result.value)
77
+
78
+ # Validate that a value responds to a method
79
+ # Usage:
80
+ # type = Types::Any.policy(respond_to: :upcase)
81
+ # type = Types::Any.policy(respond_to: [:upcase, :strip])
82
+ policy :respond_to do |type, method_names|
83
+ type.check("must respond to #{method_names.inspect}") do |value|
84
+ Array(method_names).all? { |m| value.respond_to?(m) }
85
+ end
17
86
  end
18
- Rules.define :respond_to, 'must respond to %<value>s' do |result, value|
19
- Array(value).all? { |m| result.value.respond_to?(m) }
87
+
88
+ # Return a default value if the input value is Undefined (ie key not present in a Hash).
89
+ # Usage:
90
+ # type = Types::String.default('default')
91
+ # type.parse(Undefined) # => 'default'
92
+ # type.parse('yes') # => 'yes'
93
+ #
94
+ # Works with a block too:
95
+ # date = Type::Any[Date].default { Date.today }
96
+ #
97
+ policy :default, helper: true do |type, value = Undefined, &block|
98
+ val_type = if value == Undefined
99
+ Step.new(->(result) { result.valid(block.call) }, 'default proc')
100
+ else
101
+ Types::Static[value]
102
+ end
103
+
104
+ type | (Types::Undefined >> val_type)
20
105
  end
21
- Rules.define :size, 'must be of size %<value>s', expects: :size do |result, value|
22
- value === result.value.size
106
+
107
+ # Split a string into an array. Default separator is /\s*,\s*/
108
+ # Usage:
109
+ # type = Types::String.split
110
+ # type.parse('a,b,c') # => ['a', 'b', 'c']
111
+ #
112
+ # Custom separator:
113
+ # type = Types::String.split(';')
114
+ module SplitPolicy
115
+ DEFAULT_SEPARATOR = /\s*,\s*/
116
+
117
+ def self.call(type, separator = DEFAULT_SEPARATOR)
118
+ type.invoke(:split, separator) >> Types::Array[String]
119
+ end
120
+
121
+ def self.for_type = ::String
122
+ def self.helper = false
23
123
  end
24
124
 
125
+ policy :split, SplitPolicy
126
+
25
127
  module Types
26
128
  extend TypeRegistry
27
129
 
@@ -43,17 +145,6 @@ module Plumb
43
145
  Tuple = TupleClass.new
44
146
  Hash = HashClass.new
45
147
  Interface = InterfaceClass.new
46
- # TODO: type-speficic concept of blank, via Rules
47
- Blank = (
48
- Undefined \
49
- | Nil \
50
- | String.value(BLANK_STRING) \
51
- | Hash.value(BLANK_HASH) \
52
- | Array.value(BLANK_ARRAY)
53
- )
54
-
55
- Present = Blank.invalid(errors: 'must be present')
56
- Split = String.transform(::String) { |v| v.split(/\s*,\s*/) }
57
148
 
58
149
  module Lax
59
150
  NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
data/lib/plumb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumb
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
@@ -23,7 +23,12 @@ module Plumb
23
23
  else
24
24
  :"#{(node.is_a?(::Class) ? node : node.class)}_class"
25
25
  end
26
- method_name = "visit_#{method_name}"
26
+
27
+ visit_name(method_name, node, props)
28
+ end
29
+
30
+ def visit_name(method_name, node, props = BLANK_HASH)
31
+ method_name = :"visit_#{method_name}"
27
32
  if respond_to?(method_name)
28
33
  send(method_name, node, props)
29
34
  else
data/lib/plumb.rb CHANGED
@@ -1,6 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'plumb/policies'
4
+
3
5
  module Plumb
6
+ @policies = Policies.new
7
+
8
+ def self.policies
9
+ @policies
10
+ end
11
+
12
+ # Register a policy with the given name and block.
13
+ # Optionally define a method on the Steppable method to call the policy.
14
+ # Example:
15
+ # Plumb.policy(:multiply_by, for_type: Integer, helper: true) do |step, factor, &block|
16
+ # step.transform(Integer) { |number| number * factor }
17
+ # end
18
+ #
19
+ # type = Types::Integer.multiply_by(2)
20
+ # type.parse(10) # => 20
21
+ #
22
+ # @param name [Symbol] the name of the policy
23
+ # @param opts [Hash] options for the policy
24
+ # @yield [Step, Object, &block] the step (type), policy argument, and policy block, if any.
25
+ def self.policy(name, opts = {}, &block)
26
+ name = name.to_sym
27
+ if opts.is_a?(Hash) && block_given?
28
+ for_type = opts[:for_type] || Object
29
+ helper = opts[:helper] || false
30
+ elsif opts.respond_to?(:call) && opts.respond_to?(:for_type) && opts.respond_to?(:helper)
31
+ for_type = opts.for_type
32
+ helper = opts.helper
33
+ block = opts.method(:call)
34
+ else
35
+ raise ArgumentError, 'Expected a block or a hash with :for_type and :helper keys'
36
+ end
37
+
38
+ policies.register(for_type, name, block)
39
+
40
+ return self unless helper
41
+
42
+ if Steppable.instance_methods.include?(name)
43
+ raise Policies::MethodAlreadyDefinedError, "Method #{name} is already defined on Steppable"
44
+ end
45
+
46
+ Steppable.define_method(name) do |arg = Undefined, &bl|
47
+ if arg == Undefined
48
+ policy(name, &bl)
49
+ else
50
+ policy(name, arg, &bl)
51
+ end
52
+ end
53
+
54
+ self
55
+ end
4
56
  end
5
57
 
6
58
  require 'plumb/result'
@@ -10,7 +62,6 @@ require 'plumb/any_class'
10
62
  require 'plumb/step'
11
63
  require 'plumb/and'
12
64
  require 'plumb/pipeline'
13
- require 'plumb/rules'
14
65
  require 'plumb/static_class'
15
66
  require 'plumb/value_class'
16
67
  require 'plumb/match_class'
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plumb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-11 00:00:00.000000000 Z
11
+ date: 2024-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
14
+ name: bigdecimal
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bigdecimal
28
+ name: concurrent-ruby
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -53,6 +53,7 @@ files:
53
53
  - examples/command_objects.rb
54
54
  - examples/concurrent_downloads.rb
55
55
  - examples/csv_stream.rb
56
+ - examples/env_config.rb
56
57
  - examples/programmers.csv
57
58
  - examples/weekdays.rb
58
59
  - lib/plumb.rb
@@ -72,8 +73,9 @@ files:
72
73
  - lib/plumb/not.rb
73
74
  - lib/plumb/or.rb
74
75
  - lib/plumb/pipeline.rb
76
+ - lib/plumb/policies.rb
77
+ - lib/plumb/policy.rb
75
78
  - lib/plumb/result.rb
76
- - lib/plumb/rules.rb
77
79
  - lib/plumb/schema.rb
78
80
  - lib/plumb/static_class.rb
79
81
  - lib/plumb/step.rb
@@ -87,7 +89,7 @@ files:
87
89
  - lib/plumb/value_class.rb
88
90
  - lib/plumb/version.rb
89
91
  - lib/plumb/visitor_handlers.rb
90
- homepage: ''
92
+ homepage: https://github.com/ismasan/plumb
91
93
  licenses:
92
94
  - MIT
93
95
  metadata: {}