hanami-view 2.3.1 → 3.0.0.rc1

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.
@@ -3,54 +3,70 @@
3
3
  module Hanami
4
4
  class View
5
5
  # @api private
6
- # @since 2.1.0
7
6
  class Rendering
8
7
  # @api private
9
- # @since 2.1.0
10
- attr_reader :config, :format
8
+ attr_reader :format
11
9
 
12
- # @api private
13
- # @since 2.1.0
14
10
  attr_reader :inflector, :part_builder, :scope_builder
15
11
 
16
- # @api private
17
- # @since 2.1.0
12
+ attr_reader :part_class, :part_namespace, :scope_class, :scope_namespace
13
+
14
+ # Stable identity for the underlying config snapshot.
15
+ attr_reader :cache_key
16
+
18
17
  attr_reader :context, :renderer
19
18
 
20
- # @api private
21
- # @since 2.1.0
22
- def initialize(config:, format:, context:)
23
- @config = config
19
+ def initialize(config_data:, format:, context:)
24
20
  @format = format
25
21
 
26
- @inflector = config.inflector
27
- @part_builder = config.part_builder
28
- @scope_builder = config.scope_builder
22
+ @inflector = config_data.inflector
23
+ @part_builder = config_data.part_builder
24
+ @scope_builder = config_data.scope_builder
25
+
26
+ @part_class = config_data.part_class
27
+ @part_namespace = config_data.part_namespace
28
+ @scope_class = config_data.scope_class
29
+ @scope_namespace = config_data.scope_namespace
30
+ @cache_key = config_data.object_id
29
31
 
30
32
  @context = context.dup_for_rendering(self)
31
- @renderer = Renderer.new(config)
33
+ @renderer = Renderer.new(config_data)
32
34
  end
33
35
 
36
+ # Returns the resolved name of the template or partial currently being rendered, or nil if
37
+ # no render is in progress.
38
+ #
39
+ # @return [String, nil]
40
+ #
41
+ # @api public
42
+ # @since x.x.x
43
+ def current_template_name
44
+ renderer.current_template_name
45
+ end
46
+
47
+ # Returns the stack of resolved names for the templates and partials currently being
48
+ # rendered.
49
+ #
50
+ # @return [Array<String>]
51
+ #
34
52
  # @api private
35
- # @since 2.1.0
53
+ # @since x.x.x
54
+ def current_template_names
55
+ renderer.current_template_names
56
+ end
57
+
36
58
  def template(name, scope, &block)
37
59
  renderer.template(name, format, scope, &block)
38
60
  end
39
61
 
40
- # @api private
41
- # @since 2.1.0
42
62
  def partial(name, scope, &block)
43
63
  renderer.partial(name, format, scope, &block)
44
64
  end
45
65
 
46
- # @api private
47
- # @since 2.1.0
48
66
  def part(name, value, as: nil)
49
67
  part_builder.(name, value, as: as, rendering: self)
50
68
  end
51
69
 
52
- # @api private
53
- # @since 2.1.0
54
70
  def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
55
71
  scope_builder.(name, locals: locals, rendering: self)
56
72
  end
@@ -8,47 +8,44 @@ module Hanami
8
8
  # @api private
9
9
  # @since 2.1.0
10
10
  class RenderingMissing
11
- # @api private
12
- # @since 2.1.0
13
11
  def format
14
12
  raise RenderingMissingError
15
13
  end
16
14
 
17
- # @api private
18
- # @since 2.1.0
19
15
  def context
20
16
  raise RenderingMissingError
21
17
  end
22
18
 
23
- # @api private
24
- # @since 2.1.0
25
19
  def part(_name, _value, **_options)
26
20
  raise RenderingMissingError
27
21
  end
28
22
 
29
- # @api private
30
- # @since 2.1.0
31
23
  def scope(_name = nil, _locals) # rubocop:disable Style/OptionalArguments
32
24
  raise RenderingMissingError
33
25
  end
34
26
 
35
- # @api private
36
- # @since 2.1.0
37
27
  def template(_name, _scope)
38
28
  raise RenderingMissingError
39
29
  end
40
30
 
41
- # @api private
42
- # @since 2.1.0
43
31
  def partial(_name, _scope)
44
32
  raise RenderingMissingError
45
33
  end
46
34
 
47
- # @api private
48
- # @since 2.1.0
49
35
  def inflector
50
36
  @inflector ||= Dry::Inflector.new
51
37
  end
38
+
39
+ def current_template_name
40
+ nil
41
+ end
42
+
43
+ def current_template_names
44
+ EMPTY_TEMPLATE_NAMES
45
+ end
46
+
47
+ EMPTY_TEMPLATE_NAMES = [].freeze
48
+ private_constant :EMPTY_TEMPLATE_NAMES
52
49
  end
53
50
  end
54
51
  end
@@ -19,7 +19,7 @@ module Hanami
19
19
  # @since 2.1.0
20
20
  class Scope
21
21
  # @api private
22
- CONVENIENCE_METHODS = %i[format context locals].freeze
22
+ CONVENIENCE_METHODS = %i[format context locals template_name].freeze
23
23
 
24
24
  include Dry::Equalizer(:_name, :_locals, :_rendering)
25
25
 
@@ -35,10 +35,10 @@ module Hanami
35
35
  #
36
36
  # @overload _locals
37
37
  # Returns the locals.
38
+ # @return [Hash{Symbol => Object}]
38
39
  # @overload locals
39
40
  # A convenience alias for `#_locals.` Is available unless there is a local named `locals`
40
- #
41
- # @return [Hash[<Symbol, Object>]
41
+ # @return [Hash{Symbol => Object}]
42
42
  #
43
43
  # @api public
44
44
  # @since 2.1.0
@@ -76,16 +76,16 @@ module Hanami
76
76
  # Renders a partial using the scope.
77
77
  #
78
78
  # @param partial_name [Symbol, String] partial name
79
- # @param locals [Hash<Symbol, Object>] partial locals
79
+ # @param locals [Hash{Symbol => Object}] partial locals
80
80
  # @yieldreturn [String] string content to include where the partial calls `yield`
81
+ # @return [String] the rendered partial output
81
82
  #
82
83
  # @overload render(**locals, &block)
83
84
  # Renders a partial (named after the scope's own name) using the scope.
84
85
  #
85
- # @param locals[Hash<Symbol, Object>] partial locals
86
+ # @param locals [Hash{Symbol => Object}] partial locals
86
87
  # @yieldreturn [String] string content to include where the partial calls `yield`
87
- #
88
- # @return [String] the rendered partial output
88
+ # @return [String] the rendered partial output
89
89
  #
90
90
  # @api public
91
91
  # @since 2.1.0
@@ -120,11 +120,10 @@ module Hanami
120
120
  #
121
121
  # @overload _format
122
122
  # Returns the format.
123
+ # @return [Symbol] format
123
124
  # @overload format
124
- # A convenience alias for `#_format.` Is available unless there is a
125
- # local named `format`
126
- #
127
- # @return [Symbol] format
125
+ # A convenience alias for `#_format.` Is available unless there is a local named `format`.
126
+ # @return [Symbol] format
128
127
  #
129
128
  # @api public
130
129
  # @since 2.1.0
@@ -136,11 +135,10 @@ module Hanami
136
135
  #
137
136
  # @overload _context
138
137
  # Returns the context.
138
+ # @return [Context] context
139
139
  # @overload context
140
- # A convenience alias for `#_context`. Is available unless there is a
141
- # local named `context`.
142
- #
143
- # @return [Context] context
140
+ # A convenience alias for `#_context`. Is available unless there is a local named `context`.
141
+ # @return [Context] context
144
142
  #
145
143
  # @api public
146
144
  # @since 2.1.0
@@ -148,6 +146,22 @@ module Hanami
148
146
  _rendering.context
149
147
  end
150
148
 
149
+ # Returns the name of the template or partial currently being rendered.
150
+ #
151
+ # @overload _template_name
152
+ # Returns the current template name.
153
+ # @return [String, nil]
154
+ # @overload template_name
155
+ # A convenience alias for `#_template_name`. Is available unless there is a local named
156
+ # `template_name`.
157
+ # @return [String, nil]
158
+ #
159
+ # @api public
160
+ # @since x.x.x
161
+ def _template_name
162
+ _rendering.current_template_name
163
+ end
164
+
151
165
  private
152
166
 
153
167
  # Handles missing methods, according to the following rules:
@@ -17,7 +17,7 @@ module Hanami
17
17
  #
18
18
  # @api public
19
19
  # @since 2.1.0
20
- def call(name = nil, locals:, rendering:) # rubocop:disable Style/OptionalArguments
20
+ def call(name = nil, locals:, rendering:)
21
21
  klass = scope_class(name, rendering: rendering)
22
22
 
23
23
  klass.new(name: name, locals: locals, rendering: rendering)
@@ -27,11 +27,11 @@ module Hanami
27
27
 
28
28
  def scope_class(name = nil, rendering:)
29
29
  if name.nil?
30
- rendering.config.scope_class
30
+ rendering.scope_class
31
31
  elsif name.is_a?(Class)
32
32
  name
33
33
  else
34
- View.cache.fetch_or_store(name, rendering.config) do
34
+ View.cache.fetch_or_store(name, rendering.cache_key) do
35
35
  resolve_scope_class(name: name, rendering: rendering)
36
36
  end
37
37
  end
@@ -40,12 +40,12 @@ module Hanami
40
40
  def resolve_scope_class(name:, rendering:)
41
41
  name = rendering.inflector.camelize(name.to_s)
42
42
 
43
- namespace = rendering.config.scope_namespace
43
+ namespace = rendering.scope_namespace
44
44
 
45
45
  # Give autoloaders a chance to act
46
46
  begin
47
47
  klass = namespace.const_get(name)
48
- rescue NameError # rubocop:disable Lint/HandleExceptions
48
+ rescue NameError # rubocop:disable Lint/SuppressedException
49
49
  end
50
50
 
51
51
  if !klass && namespace.const_defined?(name, false)
@@ -55,7 +55,7 @@ module Hanami
55
55
  if klass && klass < Scope
56
56
  klass
57
57
  else
58
- rendering.config.scope_class
58
+ rendering.scope_class
59
59
  end
60
60
  end
61
61
  end
@@ -34,7 +34,7 @@ module Hanami
34
34
  Template = Temple::Templates::Tilt(
35
35
  ::Haml::Engine,
36
36
  use_html_safe: true,
37
- capture_generator: HTMLSafeStringBuffer,
37
+ capture_generator: HTMLSafeStringBuffer
38
38
  )
39
39
  end
40
40
  end
@@ -34,7 +34,7 @@ module Hanami
34
34
  Template = Temple::Templates::Tilt(
35
35
  ::Slim::Engine,
36
36
  use_html_safe: true,
37
- capture_generator: HTMLSafeStringBuffer,
37
+ capture_generator: HTMLSafeStringBuffer
38
38
  )
39
39
  end
40
40
  end
@@ -3,7 +3,6 @@
3
3
  module Hanami
4
4
  class View
5
5
  # @api public
6
- # @since 0.1.0
7
- VERSION = "2.3.1"
6
+ VERSION = "3.0.0.rc1"
8
7
  end
9
8
  end
data/lib/hanami/view.rb CHANGED
@@ -22,7 +22,6 @@ module Hanami
22
22
  # @since 2.1.0
23
23
  class View
24
24
  # @api private
25
- # @since 2.1.0
26
25
  def self.gem_loader
27
26
  @gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
28
27
  root = File.expand_path("..", __dir__)
@@ -31,13 +30,13 @@ module Hanami
31
30
  loader.ignore(
32
31
  "#{root}/hanami-view.rb",
33
32
  "#{root}/hanami/view/version.rb",
34
- "#{root}/hanami/view/errors.rb",
33
+ "#{root}/hanami/view/errors.rb"
35
34
  )
36
35
  loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-view.rb")
37
36
  loader.inflector.inflect(
38
37
  "erb" => "ERB",
39
38
  "html" => "HTML",
40
- "html_safe_string_buffer" => "HTMLSafeStringBuffer",
39
+ "html_safe_string_buffer" => "HTMLSafeStringBuffer"
41
40
  )
42
41
  end
43
42
  end
@@ -45,7 +44,6 @@ module Hanami
45
44
  gem_loader.setup
46
45
 
47
46
  # @api private
48
- # @since 2.1.0
49
47
  DEFAULT_RENDERER_OPTIONS = {default_encoding: "utf-8"}.freeze
50
48
 
51
49
  include Dry::Equalizer(:config, :exposures)
@@ -235,6 +233,23 @@ module Hanami
235
233
  # @!scope class
236
234
  setting :inflector, default: Dry::Inflector.new
237
235
 
236
+ # @overload config.decorate_exposures=(value)
237
+ # Controls whether exposures are decorated by default.
238
+ #
239
+ # When set to `true`, all exposures will be decorated with matching Parts unless explicitly
240
+ # marked with `decorate: false`.
241
+ #
242
+ # When set to `false` (the default), exposures will not be decorated unless explicitly marked
243
+ # with `decorate: true`, or declared with `decorate`.
244
+ #
245
+ # Defaults to `false`.
246
+ #
247
+ # @param value [Boolean] whether to decorate exposures by default
248
+ # @api public
249
+ # @since x.x.x
250
+ # @!scope class
251
+ setting :decorate_exposures, default: false
252
+
238
253
  # @overload config.renderer_options=(options)
239
254
  # A hash of options to pass to the template engine. Template engines are
240
255
  # provided by Tilt; see Tilt's documentation for what options your
@@ -273,7 +288,6 @@ module Hanami
273
288
  # @!endgroup
274
289
 
275
290
  # @api private
276
- # @since 2.1.0
277
291
  def self.inherited(klass)
278
292
  super
279
293
 
@@ -288,13 +302,13 @@ module Hanami
288
302
  # @param options [Hash] the exposure's options
289
303
  # @option options [Boolean] :layout expose this value to the layout (defaults to false)
290
304
  # @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
291
- # true)
305
+ # false, or the value of `config.decorate_exposures`)
292
306
  # @option options [Symbol, Class] :as an alternative name or class to use when finding a
293
307
  # matching Part
294
308
 
295
309
  # @overload expose(name, **options, &block)
296
310
  # Define a value to be passed to the template. The return value of the
297
- # block will be decorated by a matching Part and passed to the template.
311
+ # block will be passed to the template.
298
312
  #
299
313
  # The block will be evaluated with the view instance as its `self`. The
300
314
  # block's parameters will determine what it is given:
@@ -329,8 +343,8 @@ module Hanami
329
343
  #
330
344
  # @overload expose(name, **options)
331
345
  # Define a value to be passed to the template, provided by an instance
332
- # method matching the name. The method's return value will be decorated by
333
- # a matching Part and passed to the template.
346
+ # method matching the name. The method's return value will be passed to
347
+ # the template.
334
348
  #
335
349
  # The method's parameters will determine what it is given:
336
350
  #
@@ -369,8 +383,8 @@ module Hanami
369
383
  #
370
384
  # @overload expose(name, **options)
371
385
  # Define a single value to pass through from the input data (when there is
372
- # no instance method matching the `name`). This value will be decorated by
373
- # a matching Part and passed to the template.
386
+ # no instance method matching the `name`). This value will be passed to
387
+ # the template.
374
388
  #
375
389
  # @param name [Symbol] name for the exposure
376
390
  # @macro exposure_options
@@ -380,7 +394,7 @@ module Hanami
380
394
  # @overload expose(*names, **options)
381
395
  # Define multiple values to pass through from the input data (when there
382
396
  # is no instance methods matching their names). These values will be
383
- # decorated by matching Parts and passed through to the template.
397
+ # passed through to the template.
384
398
  #
385
399
  # The provided options will be applied to all the exposures.
386
400
  #
@@ -411,12 +425,23 @@ module Hanami
411
425
  expose(*names, **options, private: true, &block)
412
426
  end
413
427
 
428
+ # Defines an exposure that will be decorated with a matching Part.
429
+ #
430
+ # This is a shorthand for `expose(..., decorate: true)`.
431
+ #
432
+ # @see expose
433
+ #
434
+ # @api public
435
+ # @since 2.1.0
436
+ def self.decorate(*names, **options, &block)
437
+ expose(*names, **options, decorate: true, &block)
438
+ end
439
+
414
440
  # Returns the defined exposures. These are unbound, since bound exposures
415
441
  # are only created when initializing a View instance.
416
442
  #
417
443
  # @return [Exposures]
418
444
  # @api private
419
- # @since 2.1.0
420
445
  def self.exposures
421
446
  @exposures ||= Exposures.new
422
447
  end
@@ -512,13 +537,6 @@ module Hanami
512
537
  # @!endgroup
513
538
 
514
539
  # @api private
515
- # @since 2.1.0
516
- def self.layout_path(layout)
517
- File.join(*[config.layouts_dir, layout].compact)
518
- end
519
-
520
- # @api private
521
- # @since 2.1.0
522
540
  def self.cache
523
541
  Cache
524
542
  end
@@ -534,6 +552,7 @@ module Hanami
534
552
  self.class.config.finalize!
535
553
  ensure_config
536
554
 
555
+ @config_data = config.to_data
537
556
  @exposures = self.class.exposures.bind(self)
538
557
  end
539
558
 
@@ -550,8 +569,7 @@ module Hanami
550
569
  # @return [Exposures]
551
570
  #
552
571
  # @api private
553
- # @since 2.1.0
554
- def exposures
572
+ def exposures # rubocop:disable Style/TrivialAccessors
555
573
  @exposures
556
574
  end
557
575
 
@@ -566,16 +584,20 @@ module Hanami
566
584
  #
567
585
  # @api public
568
586
  # @since 2.1.0
569
- def call(format: config.default_format, context: config.default_context, layout: config.layout, **input)
587
+ def call(format: config_data.default_format,
588
+ context: config_data.default_context,
589
+ layout: config_data.layout,
590
+ **input)
570
591
  rendering = self.rendering(format: format, context: context)
592
+ scope_class = config_data.scope
571
593
 
572
594
  locals = locals(rendering, input)
573
- output = rendering.template(config.template, rendering.scope(config.scope, locals))
595
+ output = rendering.template(config_data.template, rendering.scope(scope_class, locals))
574
596
 
575
597
  if layout
576
598
  output = rendering.template(
577
- self.class.layout_path(layout),
578
- rendering.scope(config.scope, layout_locals(locals))
599
+ layout_path(layout),
600
+ rendering.scope(scope_class, layout_locals(locals))
579
601
  ) { output }
580
602
  end
581
603
 
@@ -583,13 +605,18 @@ module Hanami
583
605
  end
584
606
 
585
607
  # @api private
586
- # @since 2.1.0
587
- def rendering(format: config.default_format, context: config.default_context)
588
- Rendering.new(config: config, format: format, context: context)
608
+ def rendering(format: config_data.default_format, context: config_data.default_context)
609
+ Rendering.new(config_data:, format:, context:)
589
610
  end
590
611
 
591
612
  private
592
613
 
614
+ # Frozen Data snapshot of the view's resolved configuration values.
615
+ # Used for fast hot-path reads during rendering.
616
+ #
617
+ # @api private
618
+ attr_reader :config_data
619
+
593
620
  def ensure_config
594
621
  raise UndefinedConfigError, :paths unless Array(config.paths).any?
595
622
  raise UndefinedConfigError, :template unless config.template
@@ -597,7 +624,7 @@ module Hanami
597
624
 
598
625
  def locals(rendering, input)
599
626
  exposures.(context: rendering.context, **input) do |value, exposure|
600
- if exposure.decorate? && value
627
+ if exposure.decorate?(default: config_data.decorate_exposures) && value
601
628
  rendering.part(exposure.name, value, as: exposure.options[:as])
602
629
  else
603
630
  value
@@ -605,6 +632,11 @@ module Hanami
605
632
  end
606
633
  end
607
634
 
635
+ # @api private
636
+ def layout_path(layout)
637
+ File.join(*[config_data.layouts_dir, layout].compact)
638
+ end
639
+
608
640
  def layout_locals(locals)
609
641
  locals.each_with_object({}) do |(key, value), layout_locals|
610
642
  layout_locals[key] = value if exposures[key].for_layout?
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-view
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 3.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hanakai team
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: '1.4'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: '1.4'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dry-core
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -105,57 +105,19 @@ dependencies:
105
105
  - - "~>"
106
106
  - !ruby/object:Gem::Version
107
107
  version: '2.6'
108
- - !ruby/object:Gem::Dependency
109
- name: bundler
110
- requirement: !ruby/object:Gem::Requirement
111
- requirements:
112
- - - ">="
113
- - !ruby/object:Gem::Version
114
- version: '0'
115
- type: :development
116
- prerelease: false
117
- version_requirements: !ruby/object:Gem::Requirement
118
- requirements:
119
- - - ">="
120
- - !ruby/object:Gem::Version
121
- version: '0'
122
- - !ruby/object:Gem::Dependency
123
- name: rake
124
- requirement: !ruby/object:Gem::Requirement
125
- requirements:
126
- - - ">="
127
- - !ruby/object:Gem::Version
128
- version: '0'
129
- type: :development
130
- prerelease: false
131
- version_requirements: !ruby/object:Gem::Requirement
132
- requirements:
133
- - - ">="
134
- - !ruby/object:Gem::Version
135
- version: '0'
136
- - !ruby/object:Gem::Dependency
137
- name: rspec
138
- requirement: !ruby/object:Gem::Requirement
139
- requirements:
140
- - - ">="
141
- - !ruby/object:Gem::Version
142
- version: '0'
143
- type: :development
144
- prerelease: false
145
- version_requirements: !ruby/object:Gem::Requirement
146
- requirements:
147
- - - ">="
148
- - !ruby/object:Gem::Version
149
- version: '0'
150
108
  description: A complete, standalone view rendering system that gives you everything
151
109
  you need to write well-factored view code
152
110
  email:
153
111
  - info@hanakai.org
154
112
  executables: []
155
113
  extensions: []
156
- extra_rdoc_files: []
114
+ extra_rdoc_files:
115
+ - CHANGELOG.md
116
+ - LICENSE
117
+ - README.md
157
118
  files:
158
119
  - CHANGELOG.md
120
+ - LICENSE
159
121
  - README.md
160
122
  - hanami-view.gemspec
161
123
  - lib/hanami-view.rb
@@ -194,11 +156,10 @@ homepage: https://hanamirb.org
194
156
  licenses:
195
157
  - MIT
196
158
  metadata:
197
- rubygems_mfa_required: 'true'
198
- allowed_push_host: https://rubygems.org
199
- changelog_uri: https://github.com/hanami/view/blob/main/CHANGELOG.md
200
- source_code_uri: https://github.com/hanami/view
201
- bug_tracker_uri: https://github.com/hanami/view/issues
159
+ changelog_uri: https://github.com/hanami/hanami-view/blob/main/CHANGELOG.md
160
+ source_code_uri: https://github.com/hanami/hanami-view
161
+ bug_tracker_uri: https://github.com/hanami/hanami-view/issues
162
+ funding_uri: https://github.com/sponsors/hanami
202
163
  rdoc_options: []
203
164
  require_paths:
204
165
  - lib
@@ -206,7 +167,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
206
167
  requirements:
207
168
  - - ">="
208
169
  - !ruby/object:Gem::Version
209
- version: '3.2'
170
+ version: '3.3'
210
171
  required_rubygems_version: !ruby/object:Gem::Requirement
211
172
  requirements:
212
173
  - - ">="