grape 4.0.1 → 4.1.0

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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/README.md +195 -95
  4. data/UPGRADING.md +364 -0
  5. data/grape.gemspec +1 -1
  6. data/lib/grape/api/instance.rb +57 -30
  7. data/lib/grape/api.rb +50 -20
  8. data/lib/grape/content_types.rb +26 -9
  9. data/lib/grape/cookies.rb +1 -1
  10. data/lib/grape/declared_params_handler.rb +35 -7
  11. data/lib/grape/dsl/declared.rb +5 -4
  12. data/lib/grape/dsl/desc.rb +1 -2
  13. data/lib/grape/dsl/entity.rb +35 -19
  14. data/lib/grape/dsl/headers.rb +3 -3
  15. data/lib/grape/dsl/helpers.rb +5 -8
  16. data/lib/grape/dsl/inside_route.rb +33 -27
  17. data/lib/grape/dsl/logger.rb +10 -4
  18. data/lib/grape/dsl/middleware.rb +4 -9
  19. data/lib/grape/dsl/mounting.rb +149 -0
  20. data/lib/grape/dsl/parameters.rb +78 -104
  21. data/lib/grape/dsl/request_response.rb +35 -22
  22. data/lib/grape/dsl/routing.rb +17 -191
  23. data/lib/grape/dsl/settings.rb +1 -1
  24. data/lib/grape/dsl/version_options.rb +2 -2
  25. data/lib/grape/dsl/versioning.rb +98 -0
  26. data/lib/grape/endpoint/state.rb +15 -0
  27. data/lib/grape/endpoint.rb +229 -137
  28. data/lib/grape/error_formatter/base.rb +28 -13
  29. data/lib/grape/error_formatter/json.rb +3 -0
  30. data/lib/grape/error_formatter/jsonl.rb +15 -0
  31. data/lib/grape/error_formatter/xml.rb +3 -1
  32. data/lib/grape/exceptions/error_response.rb +23 -7
  33. data/lib/grape/exceptions/halt.rb +39 -0
  34. data/lib/grape/exceptions/validation.rb +9 -9
  35. data/lib/grape/exceptions/validation_errors.rb +12 -2
  36. data/lib/grape/formatter/jsonl.rb +34 -0
  37. data/lib/grape/formatter/markdown.rb +17 -0
  38. data/lib/grape/formatter/md.rb +11 -0
  39. data/lib/grape/locale/en.yml +1 -0
  40. data/lib/grape/middleware/auth/base.rb +16 -1
  41. data/lib/grape/middleware/auth/strategy_info.rb +2 -2
  42. data/lib/grape/middleware/base.rb +19 -13
  43. data/lib/grape/middleware/error.rb +133 -35
  44. data/lib/grape/middleware/formatter.rb +235 -94
  45. data/lib/grape/middleware/precomputed_content_types.rb +14 -2
  46. data/lib/grape/middleware/stack.rb +7 -8
  47. data/lib/grape/middleware/versioner/base.rb +26 -10
  48. data/lib/grape/middleware/versioner/header.rb +70 -15
  49. data/lib/grape/middleware/versioner/param.rb +3 -5
  50. data/lib/grape/middleware/versioner/path.rb +20 -11
  51. data/lib/grape/middleware/versioner.rb +1 -1
  52. data/lib/grape/mountable.rb +2 -4
  53. data/lib/grape/namespace.rb +2 -3
  54. data/lib/grape/parser/jsonl.rb +19 -0
  55. data/lib/grape/request.rb +46 -8
  56. data/lib/grape/router/base_route.rb +41 -0
  57. data/lib/grape/router/greedy_route.rb +1 -1
  58. data/lib/grape/router/mustermann_pattern.rb +1 -1
  59. data/lib/grape/router/pattern/path.rb +1 -1
  60. data/lib/grape/router/pattern.rb +11 -5
  61. data/lib/grape/router/route.rb +42 -11
  62. data/lib/grape/router/route_buckets.rb +246 -0
  63. data/lib/grape/router.rb +262 -32
  64. data/lib/grape/util/endpoint_configuration.rb +7 -1
  65. data/lib/grape/util/inheritable_setting.rb +80 -161
  66. data/lib/grape/util/lazy/value.rb +52 -11
  67. data/lib/grape/util/media_type.rb +38 -15
  68. data/lib/grape/util/path_normalizer.rb +7 -2
  69. data/lib/grape/util/registry.rb +9 -2
  70. data/lib/grape/util/scrub.rb +33 -0
  71. data/lib/grape/util/translation.rb +6 -6
  72. data/lib/grape/validations/attributes_iterator.rb +73 -37
  73. data/lib/grape/validations/coerce_options.rb +1 -1
  74. data/lib/grape/validations/params_documentation.rb +15 -7
  75. data/lib/grape/validations/params_scope.rb +176 -43
  76. data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
  77. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  78. data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
  79. data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
  80. data/lib/grape/validations/types/primitive_coercer.rb +17 -0
  81. data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
  82. data/lib/grape/validations/types.rb +12 -18
  83. data/lib/grape/validations/validations_spec.rb +53 -22
  84. data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
  85. data/lib/grape/validations/validators/as_validator.rb +2 -3
  86. data/lib/grape/validations/validators/base.rb +124 -25
  87. data/lib/grape/validations/validators/coerce_validator.rb +2 -3
  88. data/lib/grape/validations/validators/default_validator.rb +18 -2
  89. data/lib/grape/validations/validators/except_values_validator.rb +16 -3
  90. data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
  91. data/lib/grape/validations/validators/length_validator.rb +24 -16
  92. data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
  93. data/lib/grape/validations/validators/values_validator.rb +18 -24
  94. data/lib/grape/version.rb +1 -1
  95. data/lib/grape.rb +3 -0
  96. metadata +18 -11
  97. data/lib/grape/util/lazy/value_array.rb +0 -21
  98. data/lib/grape/util/lazy/value_enumerable.rb +0 -31
  99. data/lib/grape/util/lazy/value_hash.rb +0 -21
  100. data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
  101. data/lib/grape/validations/single_attribute_iterator.rb +0 -28
@@ -55,6 +55,29 @@ module Grape
55
55
  @global = {}
56
56
  end
57
57
 
58
+ # Declares a nearest-wins scalar per key: a nested scope's assignment
59
+ # overrides an inherited one, hence a plain +=+ writer rather than the
60
+ # +add_*+ writers of the stackable registrations, and a reader that
61
+ # resolves the key along the chain, nil when never set. The backing
62
+ # store is an internal detail.
63
+ def self.inheritable_property(*keys)
64
+ keys.each do |key|
65
+ define_method(key) { inheritable(key) }
66
+ define_method(:"#{key}=") { |value| set_inheritable(key, value) }
67
+ end
68
+ end
69
+
70
+ # Declares a scope flag per key: a bang method setting it, after which it
71
+ # applies to the scope and everything nested under it, and a predicate,
72
+ # false when never set. The backing store is an internal detail.
73
+ def self.inheritable_flag(*keys)
74
+ keys.each do |key|
75
+ define_method(:"#{key}!") { set_inheritable(key, true) }
76
+ define_method(:"#{key}?") { inheritable(key) == true }
77
+ end
78
+ end
79
+ private_class_method :inheritable_property, :inheritable_flag
80
+
58
81
  # Instantiate a new settings instance, with blank values. The fresh
59
82
  # instance can then be set to inherit from an existing instance (see
60
83
  # #inherit_from).
@@ -99,7 +122,7 @@ module Grape
99
122
 
100
123
  # Create a point-in-time copy of this settings instance, with clones of
101
124
  # all our values. Note that, should this instance's parent be set or
102
- # changed via #inherit_from, it will copy that inheritence to any copies
125
+ # changed via #inherit_from, it will copy that inheritance to any copies
103
126
  # which were made.
104
127
  def point_in_time_copy
105
128
  new_setting = self.class.new
@@ -172,8 +195,8 @@ module Grape
172
195
  end
173
196
 
174
197
  # The route-scope settings handed to each Grape::Router::Route: every
175
- # +route_setting+ registration plus the description, minus the internal
176
- # param snapshots (#route_validations / #route_declared_params).
198
+ # +route_setting+ registration, the description and renamed params,
199
+ # minus #route_validations / #route_declared_params.
177
200
  def route_settings
178
201
  route.except(:declared_params, :validations)
179
202
  end
@@ -193,10 +216,10 @@ module Grape
193
216
  # follow the ones already seeded from the surrounding scopes.
194
217
  def inherit_route_params(parent)
195
218
  parent_validations = parent.validations
196
- route_validations.concat(parent_validations) if parent_validations.any?
219
+ route_validations.concat(parent_validations) unless parent_validations.empty?
197
220
 
198
221
  parent_declared_params = parent.declared_params
199
- route_declared_params.concat(parent_declared_params.flatten) if parent_declared_params.any?
222
+ route_declared_params.concat(parent_declared_params.flatten) unless parent_declared_params.empty?
200
223
  end
201
224
 
202
225
  # Return a serializable hash of our values.
@@ -228,12 +251,7 @@ module Grape
228
251
  # compared resolved.
229
252
  return to_hash == other.to_hash unless parent.equal?(other.parent)
230
253
 
231
- same_own_store?(@stackable_values, other.stackable_values) &&
232
- route == other.route &&
233
- @namespace_inheritable == other.namespace_inheritable &&
234
- namespace == other.namespace &&
235
- rescue_handlers == other.rescue_handlers &&
236
- base_only_rescue_handlers == other.base_only_rescue_handlers
254
+ same_own_state?(other)
237
255
  end
238
256
  alias eql? ==
239
257
 
@@ -442,9 +460,9 @@ module Grape
442
460
 
443
461
  # Middleware specs recorded by the middleware DSL (+use+, +insert+,
444
462
  # +insert_before+, +insert_after+; see DSL::Middleware), one
445
- # [operation, *arguments] Array per registration, outermost scope
446
- # first. Record them with #add_middleware; the backing store is an
447
- # internal detail.
463
+ # [operation, *arguments, block] Array per registration, the block +nil+
464
+ # when none was given, outermost scope first. Record them with
465
+ # #add_middleware; the backing store is an internal detail.
448
466
  def middleware
449
467
  stacked(:middleware)
450
468
  end
@@ -490,7 +508,7 @@ module Grape
490
508
  end
491
509
 
492
510
  # The path a Grape API is mounted under, recorded on the mounted API's
493
- # top-level settings by +mount+ (see DSL::Routing). Reading returns the
511
+ # top-level settings by +mount+ (see DSL::Mounting). Reading returns the
494
512
  # outermost mount path — nil when the API is not mounted; the backing
495
513
  # store is an internal detail.
496
514
  def mount_path
@@ -526,151 +544,51 @@ module Grape
526
544
  # +format+ is the enforced API format, +default_format+ the fallback
527
545
  # used when a request doesn't specify one, and
528
546
  # +default_error_formatter+ / +default_error_status+ shape error
529
- # responses. Nearest-wins scalars — a nested scope's assignment
530
- # overrides an inherited one, hence plain +=+ writers rather than the
531
- # +add_*+ writers used for stackable registrations. Readers return nil
532
- # when never set (Endpoint applies the request-serving fallbacks); the
533
- # backing store is an internal detail.
534
- def format
535
- inheritable(:format)
536
- end
537
-
538
- def format=(format)
539
- set_inheritable(:format, format)
540
- end
541
-
542
- def default_format
543
- inheritable(:default_format)
544
- end
545
-
546
- def default_format=(default_format)
547
- set_inheritable(:default_format, default_format)
548
- end
549
-
550
- def default_error_formatter
551
- inheritable(:default_error_formatter)
552
- end
553
-
554
- def default_error_formatter=(formatter)
555
- set_inheritable(:default_error_formatter, formatter)
556
- end
547
+ # responses. Endpoint applies the request-serving fallbacks to a nil.
548
+ inheritable_property :format, :default_format, :default_error_formatter, :default_error_status
557
549
 
558
- def default_error_status
559
- inheritable(:default_error_status)
560
- end
561
-
562
- def default_error_status=(status)
563
- set_inheritable(:default_error_status, status)
564
- end
565
-
566
- # Versioning state recorded by the routing DSL (see DSL::Routing):
550
+ # Versioning state recorded by the DSL (see DSL::Versioning and DSL::Routing):
567
551
  # +version+ holds the Array of version strings registered by the
568
552
  # +version+ DSL method, +version_options+ its DSL::VersionOptions
569
553
  # value object, and +root_prefix+ the path prefix set by +prefix+.
570
- # Nearest-wins scalars with plain += writers; readers return nil when
571
- # never set; the backing store is an internal detail.
572
- def version
573
- inheritable(:version)
574
- end
554
+ inheritable_property :version, :version_options, :root_prefix
575
555
 
576
- def version=(versions)
577
- set_inheritable(:version, versions)
578
- end
579
-
580
- def version_options
581
- inheritable(:version_options)
582
- end
556
+ # Cascade flag assigned by the +cascade+ DSL. An explicit nil is distinct
557
+ # from never-set, which #cascade_defined? tells apart; when never set,
558
+ # Grape::API::Instance falls back to the version options' cascade, then true.
559
+ inheritable_property :cascade
583
560
 
584
- def version_options=(options)
585
- set_inheritable(:version_options, options)
586
- end
587
-
588
- def root_prefix
589
- inheritable(:root_prefix)
590
- end
591
-
592
- def root_prefix=(prefix)
593
- set_inheritable(:root_prefix, prefix)
594
- end
595
-
596
- # Cascade flag assigned by the +cascade+ DSL. An explicit nil is
597
- # meaningful and distinct from never-set (the backing store is
598
- # key-presence based), so #cascade_defined? reports whether any scope
599
- # assigned it — Grape::API::Instance#cascade? falls back to the
600
- # version options' cascade, then to true, when it was never assigned.
601
- def cascade
602
- inheritable(:cascade)
603
- end
604
-
605
- def cascade=(value)
606
- set_inheritable(:cascade, value)
607
- end
561
+ # The params-builder strategy set by +build_with+ (both the
562
+ # API-level DSL::Routing#build_with and the params-block
563
+ # DSL::Parameters#build_with write it), consumed when the endpoint
564
+ # builds its Grape::Request.
565
+ inheritable_property :build_params_with
608
566
 
609
- def cascade_defined?
610
- inheritable?(:cascade)
611
- end
567
+ # The authentication configuration Hash recorded by the +auth+ DSL
568
+ # (see Middleware::Auth::DSL): {type:, proc:, **options}; nil when no
569
+ # authenticator is declared — Endpoint uses that to warn about
570
+ # unauthenticated bare Rack mounts.
571
+ inheritable_property :auth
612
572
 
613
573
  # Scope flags flipped by the routing DSL's bang methods (see
614
574
  # DSL::Routing#do_not_route_head! and friends; Validations::OneofCollector
615
- # also flips +do_not_document!+): once set in a scope they apply to it
616
- # and everything nested under it. Readers return false when never set;
617
- # the backing store is an internal detail.
618
- def do_not_route_head!
619
- set_inheritable(:do_not_route_head, true)
620
- end
575
+ # also flips +do_not_document!+).
576
+ inheritable_flag :do_not_route_head, :do_not_route_options, :do_not_document, :lint
621
577
 
622
- def do_not_route_head?
623
- inheritable(:do_not_route_head) == true
624
- end
625
-
626
- def do_not_route_options!
627
- set_inheritable(:do_not_route_options, true)
628
- end
629
-
630
- def do_not_route_options?
631
- inheritable(:do_not_route_options) == true
632
- end
633
-
634
- def do_not_document!
635
- set_inheritable(:do_not_document, true)
636
- end
637
-
638
- def do_not_document?
639
- inheritable(:do_not_document) == true
640
- end
641
-
642
- def lint!
643
- set_inheritable(:lint, true)
644
- end
645
-
646
- def lint?
647
- inheritable(:lint) == true
648
- end
649
-
650
- # The params-builder strategy set by +build_with+ (both the
651
- # API-level DSL::Routing#build_with and the params-block
652
- # DSL::Parameters#build_with write it), consumed when the endpoint
653
- # builds its Grape::Request. Nearest-wins scalar; nil when never set;
654
- # the backing store is an internal detail.
655
- def build_params_with
656
- inheritable(:build_params_with)
657
- end
658
-
659
- def build_params_with=(strategy)
660
- set_inheritable(:build_params_with, strategy)
578
+ def cascade_defined?
579
+ inheritable?(:cascade)
661
580
  end
662
581
 
663
- # The authentication configuration Hash recorded by the +auth+ DSL
664
- # (see Middleware::Auth::DSL): {type:, proc:, **options}. Nearest-wins
665
- # scalar; nil when no authenticator is declared — Endpoint uses that
666
- # to warn about unauthenticated bare Rack mounts; the backing store is
667
- # an internal detail.
668
- def auth
669
- inheritable(:auth)
582
+ # The logger assigned by the +logger+ DSL (see DSL::Logger), written to
583
+ # an API's top-level settings so that it reaches the APIs mounted under
584
+ # it. Nearest-wins scalar; nil when never set, where DSL::Logger falls
585
+ # back to its default; the backing store is an internal detail.
586
+ def logger
587
+ inheritable(:logger)
670
588
  end
671
589
 
672
- def auth=(auth_options)
673
- set_inheritable(:auth, auth_options)
590
+ def logger=(logger)
591
+ set_inheritable(:logger, logger)
674
592
  end
675
593
 
676
594
  # Immutable snapshot of the settings Router::Pattern::Path reads to
@@ -701,7 +619,7 @@ module Grape
701
619
  protected
702
620
 
703
621
  # This scope's own inheritable overrides, before inheritance; nil when
704
- # the scope overrode nothing. Peer access for #copy_state_from.
622
+ # the scope overrode nothing. Peer access for #copy_state_from and #==.
705
623
  attr_reader :namespace_inheritable
706
624
 
707
625
  # The nearest scope's value for +key+: this scope's own override when it
@@ -723,9 +641,7 @@ module Grape
723
641
  end
724
642
 
725
643
  # Every inheritable value along the chain resolved into one Hash, nearest
726
- # scope winning. Like #stacked it hands back the backing Hash when only
727
- # one scope in the chain has values, so callers must treat the result as
728
- # read-only.
644
+ # scope winning. May be the root scope's own Hash: treat it as read-only.
729
645
  def inheritable_values
730
646
  inherited = parent&.inheritable_values
731
647
  own = @namespace_inheritable
@@ -739,14 +655,12 @@ module Grape
739
655
  attr_reader :rescue_handler_maps
740
656
 
741
657
  # This scope's own stackable registrations, before inheritance; nil when
742
- # the scope registered nothing. Peer access for #copy_state_from.
658
+ # the scope registered nothing. Peer access for #copy_state_from and #==.
743
659
  attr_reader :stackable_values
744
660
 
745
- # Every registration for +key+ along the chain, outermost scope first.
746
- # Returns the frozen EMPTY_STACK when nothing is registered anywhere, and
747
- # — like the store it replaced — the backing Array itself when only this
748
- # scope registered anything, so callers must treat the result as
749
- # read-only.
661
+ # Every registration for +key+ along the chain, outermost scope first;
662
+ # the frozen EMPTY_STACK when there is none. May be the root scope's own
663
+ # Array: treat it as read-only.
750
664
  def stacked(key)
751
665
  inherited = parent&.stacked(key)
752
666
  own = @stackable_values&.[](key)
@@ -758,7 +672,7 @@ module Grape
758
672
  # Every key registered along the chain, outermost scope's keys first.
759
673
  def stacked_keys
760
674
  inherited = parent&.stacked_keys || EMPTY_STACK
761
- return inherited if @stackable_values.blank?
675
+ return inherited if @stackable_values.nil? || @stackable_values.empty?
762
676
 
763
677
  (inherited + @stackable_values.keys).uniq
764
678
  end
@@ -816,11 +730,16 @@ module Grape
816
730
  keys.each { |key| @stackable_values.delete(key) }
817
731
  end
818
732
 
819
- # Compares two lazily-allocated own-registration stores (see #stack and
820
- # #add_rescue_handlers): nil and an emptied Hash both mean "this scope
821
- # registered nothing", so #== must not tell them apart.
733
+ # #== between two scopes hanging off the same parent.
734
+ def same_own_state?(other)
735
+ same_own_store?(@stackable_values, other.stackable_values) && route == other.route && @namespace_inheritable == other.namespace_inheritable &&
736
+ namespace == other.namespace && rescue_handlers == other.rescue_handlers && base_only_rescue_handlers == other.base_only_rescue_handlers
737
+ end
738
+
739
+ # Compares two lazily-allocated own stackable stores (see #stack): nil and
740
+ # an emptied Hash both mean "registered nothing".
822
741
  def same_own_store?(mine, theirs)
823
- return theirs.blank? if mine.blank?
742
+ return theirs.nil? || theirs.empty? if mine.nil? || mine.empty?
824
743
 
825
744
  mine == theirs
826
745
  end
@@ -829,7 +748,7 @@ module Grape
829
748
  # scope winning; nil when nothing is registered.
830
749
  def namespace_stackable_with_hash(key)
831
750
  data = stacked(key)
832
- return if data.blank?
751
+ return if data.empty?
833
752
 
834
753
  data.each_with_object({}) { |value, result| result.deep_merge!(value) }
835
754
  end
@@ -3,27 +3,68 @@
3
3
  module Grape
4
4
  module Util
5
5
  module Lazy
6
+ # A read of a mount-time configuration, taken before there is one to read.
7
+ #
8
+ # A remountable API reads its configuration while its own class body runs,
9
+ # long before anybody mounts it and supplies one:
10
+ #
11
+ # class Votes < Grape::API
12
+ # get configuration[:path] do
13
+ # configuration[:response]
14
+ # end
15
+ # end
16
+ #
17
+ # Root.mount Votes, with: { path: 'votes', response: '10 votes' }
18
+ #
19
+ # +configuration[:path]+ cannot answer 'votes' yet, so it answers with a
20
+ # Value instead: the configuration it was read from, plus the path of keys
21
+ # taken to reach it. Grape records the +get+ step unevaluated and replays
22
+ # it once per mount, where {#evaluate_from} walks that same path through
23
+ # the configuration that mount supplied.
24
+ #
25
+ # Reads chain, so +configuration[:db][:host]+ and
26
+ # +configuration[:hosts][0]+ are Values holding a two-key path.
6
27
  class Value < Base
7
- attr_reader :access_keys
28
+ attr_reader :path
8
29
 
9
- def initialize(value, access_keys = [])
30
+ def initialize(config, path = [])
10
31
  super()
11
- @value = value
12
- @access_keys = access_keys
32
+ @config = config
33
+ @path = path
13
34
  end
14
35
 
15
- def evaluate_from(configuration)
16
- matching_lazy_value = configuration.fetch(@access_keys)
17
- matching_lazy_value.evaluate
36
+ # @return [Value] the same read, one key deeper.
37
+ def [](key)
38
+ Value.new(@config, @path + [key])
39
+ end
40
+
41
+ # Writes back to the configuration this was read from, backing
42
+ # <tt>API.configure { |config| config[:key] = value }</tt>.
43
+ def []=(key, value)
44
+ evaluate[key] = value
18
45
  end
19
46
 
47
+ # @return [Object] what this path holds in the configuration it was read
48
+ # from, or +nil+ where the path leads nowhere - which is every path on
49
+ # a base instance, whose configuration stays empty until it is mounted.
20
50
  def evaluate
21
- @value
51
+ at(@path)
52
+ end
53
+
54
+ # @return [Object] what this path holds in +configuration+, the
55
+ # configuration of the instance being mounted right now.
56
+ def evaluate_from(configuration)
57
+ configuration.at(@path)
22
58
  end
23
59
 
24
- def reached_by(parent_access_keys, access_key)
25
- @access_keys = parent_access_keys + [access_key]
26
- self
60
+ protected
61
+
62
+ def at(keys)
63
+ keys.reduce(@config) do |node, key|
64
+ break nil unless node.is_a?(Hash) || node.is_a?(Array)
65
+
66
+ node[key]
67
+ end
27
68
  end
28
69
  end
29
70
  end
@@ -13,14 +13,40 @@ module Grape
13
13
  # in the case they will be compared in.
14
14
  VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?<vendor>[a-z0-9.\-_!^]+?)(?:-(?<version>[a-z0-9*.]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/
15
15
 
16
- def initialize(type:, subtype:)
17
- @type = type
18
- @subtype = subtype
19
- VENDOR_VERSION_HEADER_REGEX.match(subtype) do |m|
20
- @vendor = m[:vendor]
21
- @version = m[:version]
22
- @format = m[:format]
16
+ # The pattern above cannot tell where a vendor ends and a version begins
17
+ # when either holds a hyphen, so it keeps hyphens out of the version: a
18
+ # dated version such as +2024-06-20+ read as vendor +acme-2024-06+ and
19
+ # version +20+. Given the vendor it is parsed for, the vendor is spelled
20
+ # out and everything after it up to the format is the version.
21
+ #
22
+ # Keyed by the vendors APIs declare, never by what a client sends.
23
+ class VendorVersionHeaderRegexCache < Grape::Util::Cache
24
+ def initialize
25
+ super
26
+ @cache = Hash.new do |h, vendor|
27
+ h[vendor] = /\Avnd\.(?<vendor>#{Regexp.escape(vendor)})(?:-(?<version>[a-z0-9*.-]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/
28
+ end
29
+ end
30
+ end
31
+
32
+ # Immutable, strings included: the header versioner shares one instance
33
+ # per declared media type across every request that sends it, and these
34
+ # strings are what it writes into the env. The arguments are copied
35
+ # rather than frozen, as they are the caller's.
36
+ #
37
+ # A +subtype+ naming a vendor other than +vendor+ is parsed as it would
38
+ # be without one.
39
+ def initialize(type:, subtype:, vendor: nil)
40
+ @type = -type
41
+ @subtype = -subtype
42
+ match = VendorVersionHeaderRegexCache[vendor].match(@subtype) if vendor
43
+ match ||= VENDOR_VERSION_HEADER_REGEX.match(@subtype)
44
+ if match
45
+ @vendor = match[:vendor].freeze
46
+ @version = match[:version].freeze
47
+ @format = match[:format].freeze
23
48
  end
49
+ freeze
24
50
  end
25
51
 
26
52
  def ==(other)
@@ -38,26 +64,23 @@ module Grape
38
64
  end
39
65
 
40
66
  class << self
41
- def best_quality(header, available_media_types)
42
- parse(best_quality_media_type(header, available_media_types))
67
+ def best_quality(header, available_media_types, vendor: nil)
68
+ parse(best_quality_media_type(header, available_media_types), vendor:)
43
69
  end
44
70
 
45
- def parse(media_type)
71
+ def parse(media_type, vendor: nil)
46
72
  return if media_type.blank?
47
73
 
48
74
  type, subtype = media_type.downcase.split('/', 2)
49
75
  return if type.blank? || subtype.blank?
50
76
 
51
- new(type:, subtype:)
77
+ new(type:, subtype:, vendor:)
52
78
  end
53
79
 
54
80
  def match?(media_type)
55
81
  return false if media_type.blank?
56
82
 
57
- subtype = media_type.downcase.split('/', 2).last
58
- return false if subtype.blank?
59
-
60
- VENDOR_VERSION_HEADER_REGEX.match?(subtype)
83
+ VENDOR_VERSION_HEADER_REGEX.match?(media_type.downcase.split('/', 2).last)
61
84
  end
62
85
 
63
86
  # The available types are registered in lower case and Rack matches them
@@ -19,8 +19,12 @@ module Grape
19
19
  # same predicate, and the scan stays in C without building a match.
20
20
  return path if path.start_with?('/') && !(path.end_with?('/') || path.include?('%') || path.include?('//'))
21
21
 
22
- # Slow path. The bangs below are safe because +normalized+ is a fresh
23
- # String built here, never the one the caller passed in.
22
+ normalize(path)
23
+ end
24
+
25
+ # The slow path. The bangs are safe because +normalized+ is a fresh String
26
+ # built here, never the one the caller passed in.
27
+ def self.normalize(path)
24
28
  encoding = path.encoding
25
29
  normalized = "/#{path}"
26
30
  normalized.squeeze!('/')
@@ -32,6 +36,7 @@ module Grape
32
36
 
33
37
  normalized.force_encoding(encoding)
34
38
  end
39
+ private_class_method :normalize
35
40
  end
36
41
  end
37
42
  end
@@ -8,7 +8,7 @@ module Grape
8
8
  return if short_name.nil?
9
9
 
10
10
  warn "#{short_name} is already registered with class #{registry[short_name]}. It will be overridden globally with the following: #{klass.name}" if registry.key?(short_name)
11
- registry[short_name] = registry[short_name.to_sym] = klass
11
+ @registry = registry.merge(short_name => klass, short_name.to_sym => klass).freeze
12
12
  end
13
13
 
14
14
  private
@@ -29,8 +29,15 @@ module Grape
29
29
  #
30
30
  # +register+ derives the short name as a String, so the Symbol is the
31
31
  # alias.
32
+ #
33
+ # Registration replaces the Hash instead of writing into it, and freezes
34
+ # the replacement: a registry is written when a formatter, parser,
35
+ # params builder, validator or versioner class is defined, and from then
36
+ # on it is only ever looked up -- on the request path, at that. Nothing
37
+ # holding a registry can find it changed underneath, and nothing can
38
+ # write to one without going through +register+.
32
39
  def registry
33
- @registry ||= {}
40
+ @registry ||= {}.freeze
34
41
  end
35
42
  end
36
43
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ # Outside input -- a param, a header, a query value, a body a route sets
6
+ # -- can hold bytes its encoding does not allow: a query string
7
+ # percent-encoding half a character, say. Much of String raises on such a
8
+ # value (+blank?+'s whitespace regexp, +match?+, +downcase+), so it is
9
+ # scrubbed, or asked about through {.blank?}, before anything else reads
10
+ # it.
11
+ #
12
+ # Called on the module rather than included: an included +blank?+ that
13
+ # takes an argument would shadow +Object#blank?+, which +present?+ calls.
14
+ module Scrub
15
+ # +value+ with each invalid byte replaced, or +value+ itself when it
16
+ # holds none or is not a String.
17
+ def self.scrub(value)
18
+ return value if !value.respond_to?(:valid_encoding?) || value.valid_encoding?
19
+
20
+ value.scrub
21
+ end
22
+
23
+ # +value.blank?+, answered without the copy +scrub(value).blank?+ would
24
+ # build: a String holding an invalid byte is never blank, since an
25
+ # invalid byte is not whitespace.
26
+ def self.blank?(value)
27
+ return false if value.is_a?(String) && !value.valid_encoding?
28
+
29
+ value.blank?
30
+ end
31
+ end
32
+ end
33
+ end
@@ -6,9 +6,7 @@ module Grape
6
6
  FALLBACK_LOCALE = :en
7
7
  private_constant :FALLBACK_LOCALE
8
8
  # Sentinel returned by I18n when a key is missing (passed as the default:
9
- # value). Using a named class rather than plain Object.new makes it
10
- # identifiable in debug output and immune to backends that call .to_s on
11
- # the default before returning it.
9
+ # value). Its +inspect+ names it in debug output.
12
10
  MISSING = Class.new { def inspect = 'Grape::Util::Translation::MISSING' }.new.freeze
13
11
  private_constant :MISSING
14
12
 
@@ -24,10 +22,12 @@ module Grape
24
22
  message = ::I18n.translate(key, **i18n_opts)
25
23
  return message unless message.equal?(MISSING)
26
24
 
27
- effective_default = default.equal?(MISSING) ? [*Array(scope), key].join('.') : default
28
- return effective_default if fallback_locale?(locale) || fallback_locale_unavailable?
25
+ # Only a call without +default:+ gets here: I18n answers any other with
26
+ # that default, so a miss falls back to the key's full path instead.
27
+ scoped_key = [*Array(scope), key].join('.')
28
+ return scoped_key if fallback_locale?(locale) || fallback_locale_unavailable?
29
29
 
30
- ::I18n.translate(key, default: effective_default, scope:, locale: FALLBACK_LOCALE, **)
30
+ ::I18n.translate(key, default: scoped_key, scope:, locale: FALLBACK_LOCALE, **)
31
31
  end
32
32
 
33
33
  def fallback_locale?(locale)