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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +50 -20
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- 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
|
|
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
|
|
176
|
-
#
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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,
|
|
446
|
-
#
|
|
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::
|
|
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.
|
|
530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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!+)
|
|
616
|
-
|
|
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
|
|
623
|
-
inheritable(:
|
|
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
|
|
664
|
-
#
|
|
665
|
-
# scalar; nil when
|
|
666
|
-
# to
|
|
667
|
-
|
|
668
|
-
|
|
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
|
|
673
|
-
set_inheritable(:
|
|
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.
|
|
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
|
-
#
|
|
747
|
-
#
|
|
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.
|
|
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
|
-
#
|
|
820
|
-
|
|
821
|
-
|
|
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.
|
|
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.
|
|
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 :
|
|
28
|
+
attr_reader :path
|
|
8
29
|
|
|
9
|
-
def initialize(
|
|
30
|
+
def initialize(config, path = [])
|
|
10
31
|
super()
|
|
11
|
-
@
|
|
12
|
-
@
|
|
32
|
+
@config = config
|
|
33
|
+
@path = path
|
|
13
34
|
end
|
|
14
35
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
data/lib/grape/util/registry.rb
CHANGED
|
@@ -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
|
|
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).
|
|
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
|
-
|
|
28
|
-
|
|
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:
|
|
30
|
+
::I18n.translate(key, default: scoped_key, scope:, locale: FALLBACK_LOCALE, **)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def fallback_locale?(locale)
|