roda-contrib 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb288c9a14fff9c8e20f4c634d98dbb978d16665
4
- data.tar.gz: 1796bec3454a2debbebfb7fca336b7eae91877f5
3
+ metadata.gz: 02c8c0cc04c5707343034068120ae9b8ed70cf90
4
+ data.tar.gz: af47f15ba933ef6a3c0dc06219a6e1a0a1f2d7dd
5
5
  SHA512:
6
- metadata.gz: dc3874788ae3fd3a0e937f7b45983993c07e600ab2da16158e929a7165d9f3ade248b774f200d4a4bee09845f52f6a5e09e0a951c68f86f46f6f5d1af7290c63
7
- data.tar.gz: a392f95361ea4f3d54384f5f968a09d7200ca3bad051b5e942c07a3dc229414d715b39bac5708570e02b8b78eac38ba7c6236457ce91f577dd32155601b34905
6
+ metadata.gz: 309ecb8592c05a838d39658cfad67c1d121646cc64ad0d7c54ade11ce5fadfacfb395743ddffe38d76a4de02287a401488d40c48abb965182b7cff2a59caeaa8
7
+ data.tar.gz: 5b9127a06f6c68c9cb3050713c923263e4cae9fdb01477b05ad1cc2238c4fc0a046a2ecfea7f2a1ef97ce090bb16f02d6a75023240b0071baf8acc8a1ba183a2
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --no-cache
2
+ --no-save
3
+ --output-dir www
4
+ lib/**/*.rb
5
+ -
6
+ LICENSE.txt
data/CHANGELOG ADDED
@@ -0,0 +1,8 @@
1
+ = 0.2.0(2017-6-6)
2
+ * add load_all plugin
3
+ * add multi_dispatch plugin
4
+ * add csrf plugin
5
+ * add CHANGELOG
6
+
7
+ = 0.1.0(2017-6-5)
8
+ * initial commit
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # RodaContrib
2
2
 
3
- The roda-contrib gem is my personal collection of plugins working with Roda(the Routing tree toolkit).
3
+ The roda-contrib gem is my personal collection of plugins working with Roda(the
4
+ Routing tree toolkit).
4
5
 
5
6
  ## Installation
6
7
 
@@ -20,17 +21,36 @@ Or install it yourself as:
20
21
 
21
22
  ## Usage
22
23
 
23
- <!-- TODO: Write usage instructions here -->
24
+ Currently, the roda-contrib gem ships 3 plugins:
25
+
26
+ * load\_all
27
+ * multi\_dispatch
28
+ * csrf
29
+
30
+ When loading plugins from this gem, you should append the 'contrib' to the
31
+ symbol to load it. For example, if you want to use the multi\_dispatch plugin:
32
+
33
+ ```ruby
34
+ class App < Roda
35
+ plugin :contrib_multi_dispatch
36
+ end
37
+ ```
38
+
39
+ For details of the each plugin, please refer to the documentation or the code.
24
40
 
25
41
  ## Development
26
42
 
27
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
43
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
44
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
45
+ prompt that will allow you to experiment.
28
46
 
29
47
  ## Contributing
30
48
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/luciusgone/roda-contrib.
49
+ Bug reports and pull requests are welcome on GitHub at
50
+ https://github.com/luciusgone/roda-contrib.
32
51
 
33
52
 
34
53
  ## License
35
54
 
36
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
55
+ The gem is available as open source under the terms of the
56
+ [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,6 +1,11 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'yard'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
7
+ YARD::Rake::YardocTask.new do |t|
8
+ t.stats_options = ['--list-undoc']
9
+ end
10
+
6
11
  task :default => :spec
@@ -0,0 +1,3 @@
1
+ = '0.1.0' release note
2
+
3
+ Initial release
@@ -0,0 +1,19 @@
1
+ = 0.2.0 release note
2
+
3
+ == New Features
4
+
5
+ * add _contrib_load_all_ plugin.
6
+ This plugin helps writing clean code requiring different part of the roda
7
+ app.
8
+
9
+ * add _contrib_multi_dispatch_ plugin.
10
+ This plugin helps seperating different logics by delegating request to an
11
+ external object.
12
+
13
+ * add _contrib_csrf_ plugin.
14
+ Apart from the official csrf plugin, It exposes only one interface to make
15
+ the main roda app a bit clean.
16
+
17
+ * add RodaContrib::Action
18
+ This mixin is the main way for roda-contrib to package plugable business
19
+ logic.
@@ -0,0 +1,30 @@
1
+ require 'forwardable'
2
+ require 'roda/contrib/action/dispatchable'
3
+
4
+ module RodaContrib
5
+ # The mixin is intended to use with the multi_dispatch plugin. It ships with
6
+ # Dispatchable module by default. Currently, it will load all modules defined
7
+ # under the RodaContrib::Action module
8
+ #
9
+ # If you want to use this mixin seperately, you must define the delegators
10
+ # properly:
11
+ #
12
+ # SomeDispathcer.define_delegators(RodaApp)
13
+ #
14
+ # Examples:
15
+ # see the multi_dispatch plugin
16
+ module Action
17
+ def self.included(base)
18
+ mod = self
19
+
20
+ base.class_eval do
21
+ extend ::Forwardable
22
+
23
+ mod.constants.each do |k|
24
+ c = mod.const_get(k)
25
+ include c if c.is_a? Module
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
1
+ module RodaContrib
2
+ module Action
3
+ # Basic functionalities for +RodaContrib::Action+ mixin
4
+ #
5
+ # The call method is already taken by the roda app, so we have to introduce
6
+ # a new method finish to actually executing the route block.
7
+ module Dispatchable
8
+ def self.included(base)
9
+ base.class_eval do
10
+ include InstanceMethods
11
+ extend ClassMethods
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ def initialize(scope)
17
+ @scope = scope
18
+ end
19
+
20
+ attr_reader :scope
21
+
22
+ def finish
23
+ blk = self.class.route_block
24
+ instance_exec(request, &blk)
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ attr_reader :route_block
30
+
31
+ def route(&block)
32
+ @route_block = block
33
+ end
34
+
35
+ def define_delegators(app)
36
+ m = app.instance_methods - Object.instance_methods
37
+ def_delegators :@scope, *m
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ require 'rack/csrf'
2
+
3
+ module RodaContrib
4
+ module Plugins
5
+ # The contrib_csrf plugin is a bit different from offical csrf plugin. It
6
+ # offers only one instance methods instead of five. It is just my personal
7
+ # flavor.
8
+ #
9
+ # You can use it like this:
10
+ # # in the routing tree
11
+ # csrf.field # equivalent to offical csrf_field
12
+ # csrf.header # equivalent to offical csrf_header
13
+ # csrf.metatag({}) # equivalent to offical csrf_metatag({})
14
+ # csrf.tag # equivalent to offical csrf_tag
15
+ # csrf.token # equivalent to offical csrf_token
16
+ #
17
+ # see alse Roda offical csrf plugin documentations
18
+ module Csrf
19
+ CSRF = ::Rack::Csrf
20
+
21
+ def self.configure(app, opts={})
22
+ return if opts[:skip_middleware]
23
+ app.instance_exec do
24
+ @middleware.each do |(mid, *rest), _|
25
+ if mid.equal?(CSRF)
26
+ rest[0].merge!(opts)
27
+ build_rack_app
28
+ return
29
+ end
30
+ end
31
+ use CSRF, opts
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ class CsrfDecorator
37
+ def initialize(env)
38
+ @env = env
39
+ end
40
+
41
+ def field
42
+ CSRF.field
43
+ end
44
+
45
+ def header
46
+ CSRF.header
47
+ end
48
+
49
+ def metatag(opts={})
50
+ CSRF.metatag(@env, opts)
51
+ end
52
+
53
+ def tag
54
+ CSRF.tag(@env)
55
+ end
56
+
57
+ def token
58
+ CSRF.token(@env)
59
+ end
60
+ end
61
+
62
+ def csrf
63
+ @_csrf ||= CsrfDecorator.new(env)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,42 @@
1
+ module RodaContrib
2
+ module Plugins
3
+ # the load_all plugin helps to write clean code loading related parts of
4
+ # the main roda app.
5
+ #
6
+ # It favors the +:root+ option of the roda app. If you set the +:root+
7
+ # option before you load this plugin, you can omit the +:root+ option of
8
+ # the plugin.
9
+ #
10
+ # Besides, you can load multiple directories at the same time, if loading
11
+ # order does not matter.
12
+ #
13
+ # === Example
14
+ # class App < Roda
15
+ # plugin :contrib_load_all, root: __dir__
16
+ #
17
+ # # load multiple resources at the same time
18
+ # load_all :models, :views
19
+ # # load only one folder
20
+ # load_all :helpers
21
+ # end
22
+ #
23
+ # It will immediately load models and helpers files under the defined root
24
+ # dir.
25
+ module LoadAll
26
+ def self.configure(app, opts={})
27
+ raise ArgumentError, 'Invalid root option' unless app.opts[:root] || opts[:root]
28
+ app.opts[:root] = opts[:root] if opts[:root]
29
+ end
30
+
31
+ module ClassMethods
32
+ def load_all(*rcs)
33
+ rcs.each do |rc|
34
+ patterns = File.expand_path "./#{rc}/**/*.rb", opts[:root]
35
+ Dir[patterns].each { |f| require f }
36
+ end
37
+ nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,208 @@
1
+ require 'roda/contrib/action'
2
+
3
+ module RodaContrib
4
+ module Plugins
5
+ # The +multi_dispatch+ plugin let you process the your request in a
6
+ # different context rather than the main roda app. It borrows a lot of code
7
+ # from the official +multi_route+ plugin.
8
+ #
9
+ # === Rationals
10
+ # The original rational behind it is simple: avoiding huge interface which
11
+ # is hard to maintain. The reason why this approach is viable is that
12
+ # all routing methods lives within the +Roda::RodaRequest+ class. Thus,
13
+ # wherever you can access the request object, you can route and proceed
14
+ # your request.
15
+ #
16
+ # === Features
17
+ # The +RodaContrib+ ships a mixin to make the plugin work. You can also use
18
+ # this plugin in a more object oriented way.
19
+ #
20
+ # Just like +multi_route+ plugin support namespaced routes, +multi_dispatch+
21
+ # plugin support namespace but a bit differently. Also there can be an
22
+ # extra block pass to the +multi_dispatch+ method to handle requests not
23
+ # handled by dispatchers.
24
+ #
25
+ # === Things you should take care of
26
+ # The +multi_dispatch+ plugin should work fine with most of other plugins.
27
+ # The +render+ plugin is an exception, for it is evaluated in the roda app
28
+ # instance. You should either specify the scope option explicitly or use
29
+ # the +view_options+ plugin to set the scope in the routing tree before you
30
+ # render anything.
31
+ #
32
+ # There is one more thing you should pay attention to. You should always
33
+ # define dispatch after you load all the plugins and the helper mixins into
34
+ # your class.
35
+ #
36
+ # === Example
37
+ # class App < Roda
38
+ # plugin :contrib_multi_dispatch
39
+ #
40
+ # # define dispatchers
41
+ # dispatch 'hello' do
42
+ # def say_hello
43
+ # 'hello'
44
+ # end
45
+ #
46
+ # route do |r|
47
+ # r.get do
48
+ # say_hello
49
+ # end
50
+ # end
51
+ # end
52
+ #
53
+ # dispatch 'hi' do
54
+ # # we can define method with the same name in different dispatchers
55
+ # def say_hello
56
+ # 'said hi instead of hello'
57
+ # end
58
+ #
59
+ # route do |r|
60
+ # r.get do
61
+ # say_hello
62
+ # end
63
+ # end
64
+ # end
65
+ #
66
+ # # namespaced dispatcher
67
+ # dispatch 'yarn', namespace: 'somebody' do
68
+ # def yarn
69
+ # 'somebody yarned'
70
+ # end
71
+ #
72
+ # route do |r|
73
+ # r.get do
74
+ # yarn
75
+ # end
76
+ # end
77
+ # end
78
+ #
79
+ # # class based dispatcher
80
+ # class SayGoodbye
81
+ # include ::RodaContrib::Action
82
+ #
83
+ # def say_good_bye
84
+ # 'goodbye'
85
+ # end
86
+ #
87
+ # route do |r|
88
+ # r.get do
89
+ # say_good_bye
90
+ # end
91
+ # end
92
+ # end
93
+ #
94
+ # dispatch 'goodbye', to: SayGoodbye
95
+ #
96
+ # route do |r|
97
+ # # dispatch request without namespace
98
+ # r.multi_dispatch
99
+ #
100
+ # r.on 'somebody' do
101
+ # # dispatch namespaced dispatchers
102
+ # r.multi_dispatch('somebody')
103
+ # end
104
+ # end
105
+ # end
106
+ #
107
+ # the example above shows you how to add dispatchers, how to use namespaced
108
+ # dispatchers, how to use class based dispatchers, how to dispatch request
109
+ # to dispatchers.
110
+ module MultiDispatch
111
+ def self.configure(app)
112
+ app.opts[:namespaced_dispatchers] ||= {}
113
+ app::RodaRequest.instance_variable_set(:@namespaced_dispatchers_regexps, {})
114
+ app.opts[:namespaced_dispatchers].each do |ns, dispatchers|
115
+ app::RodaRequest.build_named_dispatcher_regexp!(ns)
116
+ end
117
+ end
118
+
119
+ module ClassMethods
120
+ def freeze
121
+ opts[:namespaced_dispatchers].freeze
122
+ opts[:namespaced_dispatchers].each_value(&:freeze)
123
+ super
124
+ end
125
+
126
+ def inherited(subclass)
127
+ super
128
+ sub_ndps = subclass.opts[:namespaced_dispatchers] = {}
129
+ opts[:namespaced_dispatchers].each do |k1, v1|
130
+ sub_ndps[k1] = {}
131
+ v1.each do |k2, v2|
132
+ sub_ndps[k1][k2] = v2.dup
133
+ end
134
+ end
135
+ subclass::RodaRequest.instance_variable_set(:@namespaced_dispatchers_regexps, {})
136
+ sub_ndps.each { |k, v| subclass::RodaRequest.build_named_dispatcher_regexp!(k) }
137
+ end
138
+
139
+ def named_dispathers(namespace=nil)
140
+ targets = opts[:namespaced_dispatchers][namespace]
141
+ targets ? targets.keys : []
142
+ end
143
+
144
+ def named_dispatcher(name, namespace=nil)
145
+ opts[:namespaced_dispatchers][namespace][name]
146
+ end
147
+
148
+ def dispatch(name, namespace: nil, to: nil, &block)
149
+ opts[:namespaced_dispatchers][namespace] ||= {}
150
+
151
+ if dispatcher = opts[:namespaced_dispatchers][namespace][name]
152
+ dispatcher.class_eval(&block)
153
+ elsif to.nil?
154
+ roda_app = self
155
+ dispatcher = Class.new do
156
+ include ::RodaContrib::Action
157
+ define_delegators(roda_app)
158
+ class_eval(&block)
159
+ end
160
+ else
161
+ to.define_delegators(self)
162
+ end
163
+
164
+ opts[:namespaced_dispatchers][namespace][name] = dispatcher || to
165
+ self::RodaRequest.clear_named_dispatcher_regexp!(namespace)
166
+ self::RodaRequest.build_named_dispatcher_regexp!(namespace)
167
+ end
168
+ end
169
+
170
+ module RequestClassMethods
171
+ def clear_named_dispatcher_regexp!(namespace=nil)
172
+ @namespaced_dispatchers_regexps.delete(namespace)
173
+ end
174
+
175
+ def build_named_dispatcher_regexp!(namespace=nil)
176
+ @namespaced_dispatchers_regexps[namespace] = /(#{Regexp.union(dispatcher_ary(namespace))})/
177
+ end
178
+
179
+ def named_dispatcher_regexp(namespace=nil)
180
+ @namespaced_dispatchers_regexps[namespace]
181
+ end
182
+
183
+ private
184
+ def dispatcher_ary(namespace)
185
+ roda_class.named_dispathers(namespace).select{ |s| s.is_a?(String) }.sort.reverse
186
+ end
187
+ end
188
+
189
+ module RequestMethods
190
+ def multi_dispatch(namespace=nil)
191
+ on self.class.named_dispatcher_regexp(namespace) do |section|
192
+ r = dispatch(section, namespace: namespace)
193
+ if block_given?
194
+ yield
195
+ else
196
+ r
197
+ end
198
+ end
199
+ end
200
+
201
+ def dispatch(name, namespace: nil)
202
+ dispatcher = roda_class.named_dispatcher(name, namespace)
203
+ dispatcher.new(scope).finish
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -1,3 +1,3 @@
1
1
  module RodaContrib
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
@@ -0,0 +1,3 @@
1
+ require 'roda/contrib/plugins/csrf'
2
+
3
+ Roda::RodaPlugins.register_plugin(:contrib_csrf, RodaContrib::Plugins::Csrf)
@@ -0,0 +1,3 @@
1
+ require 'roda/contrib/plugins/load_all'
2
+
3
+ Roda::RodaPlugins.register_plugin(:contrib_load_all, RodaContrib::Plugins::LoadAll)
@@ -0,0 +1,3 @@
1
+ require 'roda/contrib/plugins/multi_dispatch'
2
+
3
+ Roda::RodaPlugins.register_plugin(:contrib_multi_dispatch, RodaContrib::Plugins::MultiDispatch)
data/roda-contrib.gemspec CHANGED
@@ -14,8 +14,10 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = 'https://github.com/luciusgone/roda-contrib'
15
15
  spec.license = 'MIT'
16
16
 
17
+ spec.required_ruby_version = ">= 2.3.1"
18
+
17
19
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features|bin)/})
20
+ f.match(%r{^(test|spec|features|bin|www)/})
19
21
  end
20
22
  spec.bindir = 'exe'
21
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -24,4 +26,9 @@ Gem::Specification.new do |spec|
24
26
  spec.add_development_dependency 'bundler', '~> 1.14'
25
27
  spec.add_development_dependency 'rake', '~> 10.0'
26
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_development_dependency 'yard'
30
+ spec.add_development_dependency 'rack-test'
31
+ spec.add_development_dependency 'rack_csrf'
32
+
33
+ spec.add_dependency 'roda', '~> 2.0'
27
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda-contrib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - luciusgone
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-05 00:00:00.000000000 Z
11
+ date: 2017-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,62 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-test
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rack_csrf
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: roda
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
55
111
  description: Small collection of my personal plugins for Roda
56
112
  email:
57
113
  - luciusgone@gmail.com
@@ -62,12 +118,24 @@ files:
62
118
  - ".gitignore"
63
119
  - ".rspec"
64
120
  - ".travis.yml"
121
+ - ".yardopts"
122
+ - CHANGELOG
65
123
  - Gemfile
66
124
  - LICENSE.txt
67
125
  - README.md
68
126
  - Rakefile
127
+ - doc/release_notes/0_1_0.rdoc
128
+ - doc/release_notes/0_2_0.rdoc
69
129
  - lib/roda/contrib.rb
130
+ - lib/roda/contrib/action.rb
131
+ - lib/roda/contrib/action/dispatchable.rb
132
+ - lib/roda/contrib/plugins/csrf.rb
133
+ - lib/roda/contrib/plugins/load_all.rb
134
+ - lib/roda/contrib/plugins/multi_dispatch.rb
70
135
  - lib/roda/contrib/version.rb
136
+ - lib/roda/plugins/contrib_csrf.rb
137
+ - lib/roda/plugins/contrib_load_all.rb
138
+ - lib/roda/plugins/contrib_multi_dispatch.rb
71
139
  - roda-contrib.gemspec
72
140
  homepage: https://github.com/luciusgone/roda-contrib
73
141
  licenses:
@@ -81,7 +149,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
81
149
  requirements:
82
150
  - - ">="
83
151
  - !ruby/object:Gem::Version
84
- version: '0'
152
+ version: 2.3.1
85
153
  required_rubygems_version: !ruby/object:Gem::Requirement
86
154
  requirements:
87
155
  - - ">="