modern 0.4.2
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 +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +173 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +36 -0
- data/Rakefile +10 -0
- data/TODOS.md +4 -0
- data/bin/console +9 -0
- data/bin/setup +8 -0
- data/example/Gemfile +9 -0
- data/example/Gemfile.lock +102 -0
- data/example/config.ru +11 -0
- data/example/example.rb +19 -0
- data/lib/modern/app/error_handling.rb +45 -0
- data/lib/modern/app/request_handling/input_handling.rb +65 -0
- data/lib/modern/app/request_handling/output_handling.rb +54 -0
- data/lib/modern/app/request_handling/request_container.rb +55 -0
- data/lib/modern/app/request_handling.rb +70 -0
- data/lib/modern/app/router.rb +27 -0
- data/lib/modern/app/trie_router.rb +37 -0
- data/lib/modern/app.rb +82 -0
- data/lib/modern/capsule.rb +17 -0
- data/lib/modern/configuration.rb +16 -0
- data/lib/modern/core_ext/array.rb +23 -0
- data/lib/modern/core_ext/hash.rb +17 -0
- data/lib/modern/descriptor/content.rb +14 -0
- data/lib/modern/descriptor/converters/input/base.rb +29 -0
- data/lib/modern/descriptor/converters/input/json.rb +21 -0
- data/lib/modern/descriptor/converters/output/base.rb +25 -0
- data/lib/modern/descriptor/converters/output/json.rb +48 -0
- data/lib/modern/descriptor/converters/output/yaml.rb +21 -0
- data/lib/modern/descriptor/converters.rb +4 -0
- data/lib/modern/descriptor/core.rb +63 -0
- data/lib/modern/descriptor/info.rb +27 -0
- data/lib/modern/descriptor/parameters.rb +149 -0
- data/lib/modern/descriptor/request_body.rb +13 -0
- data/lib/modern/descriptor/response.rb +26 -0
- data/lib/modern/descriptor/route.rb +93 -0
- data/lib/modern/descriptor/security.rb +104 -0
- data/lib/modern/descriptor/server.rb +12 -0
- data/lib/modern/descriptor.rb +15 -0
- data/lib/modern/doc_generator/open_api3/operations.rb +114 -0
- data/lib/modern/doc_generator/open_api3/paths.rb +24 -0
- data/lib/modern/doc_generator/open_api3/schema_default_types.rb +50 -0
- data/lib/modern/doc_generator/open_api3/schemas.rb +171 -0
- data/lib/modern/doc_generator/open_api3/security_schemes.rb +15 -0
- data/lib/modern/doc_generator/open_api3.rb +141 -0
- data/lib/modern/dsl/info.rb +39 -0
- data/lib/modern/dsl/response_builder.rb +41 -0
- data/lib/modern/dsl/root.rb +38 -0
- data/lib/modern/dsl/route_builder.rb +130 -0
- data/lib/modern/dsl/scope.rb +144 -0
- data/lib/modern/dsl/scope_settings.rb +39 -0
- data/lib/modern/dsl.rb +14 -0
- data/lib/modern/errors/error.rb +7 -0
- data/lib/modern/errors/setup_errors.rb +11 -0
- data/lib/modern/errors/web_errors.rb +83 -0
- data/lib/modern/errors.rb +3 -0
- data/lib/modern/redirect.rb +30 -0
- data/lib/modern/request.rb +34 -0
- data/lib/modern/response.rb +39 -0
- data/lib/modern/services.rb +17 -0
- data/lib/modern/struct.rb +25 -0
- data/lib/modern/types.rb +41 -0
- data/lib/modern/util/header_parsing.rb +27 -0
- data/lib/modern/util/trie_node.rb +53 -0
- data/lib/modern/version.rb +6 -0
- data/lib/modern.rb +8 -0
- data/manual/01-why_modern.md +115 -0
- data/modern.gemspec +54 -0
- metadata +439 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/struct'
|
4
|
+
|
5
|
+
require 'modern/capsule'
|
6
|
+
require 'modern/descriptor'
|
7
|
+
|
8
|
+
require 'modern/dsl/scope_settings'
|
9
|
+
|
10
|
+
require 'deep_dup'
|
11
|
+
require 'docile'
|
12
|
+
require 'ice_nine'
|
13
|
+
|
14
|
+
module Modern
|
15
|
+
module DSL
|
16
|
+
class Scope
|
17
|
+
attr_reader :settings
|
18
|
+
attr_reader :descriptor
|
19
|
+
|
20
|
+
def initialize(descriptor, settings = nil)
|
21
|
+
@descriptor = descriptor
|
22
|
+
@settings = settings&.dup || ScopeSettings.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def capsule(cap)
|
26
|
+
raise "Must be a Modern::Capsule." unless cap.is_a?(Modern::Capsule)
|
27
|
+
@descriptor = _scope({}, &cap.block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def path(p, &block)
|
31
|
+
p_segs = p.split("/")
|
32
|
+
new_path_segments = ([@settings.path_segments] + p_segs).flatten
|
33
|
+
@descriptor = _scope(path_segments: new_path_segments, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_response(&block)
|
37
|
+
resp = ResponseBuilder.evaluate(@settings.default_response, &block)
|
38
|
+
@settings = @settings.copy(default_response: resp)
|
39
|
+
end
|
40
|
+
|
41
|
+
def deprecate!
|
42
|
+
@settings = @settings.copy(deprecated: true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def tag(t)
|
46
|
+
@settings = @settings.copy(tags: @settings.tags + [t.to_s])
|
47
|
+
end
|
48
|
+
|
49
|
+
def helper(h)
|
50
|
+
@settings = @settings.copy(helpers: @settings.helpers + [h])
|
51
|
+
end
|
52
|
+
|
53
|
+
def parameter(name, parameter_type, opts)
|
54
|
+
param = Modern::Descriptor::Parameters.from_inputs(name, parameter_type, opts)
|
55
|
+
raise "Duplicate parameter '#{name}'.'" if @settings.parameters.any? { |p| p.name == param.name }
|
56
|
+
|
57
|
+
@settings = @settings.copy(parameters: @settings.parameters + [param])
|
58
|
+
end
|
59
|
+
|
60
|
+
def clear_security!
|
61
|
+
@settings = @settings.copy(security: [])
|
62
|
+
end
|
63
|
+
|
64
|
+
def security(sec)
|
65
|
+
@settings = @settings.copy(security: @settings.security + [sec])
|
66
|
+
end
|
67
|
+
|
68
|
+
def input_converter(media_type_or_converter, &block)
|
69
|
+
if media_type_or_converter.is_a?(Modern::Descriptor::Converters::Input::Base)
|
70
|
+
@settings = @settings.copy(input_converters: @settings.input_converters + [media_type_or_converter])
|
71
|
+
elsif media_type_or_converter.is_a?(String) && !block.nil?
|
72
|
+
input_converter(
|
73
|
+
Modern::Descriptor::Converters::Input::Base.new(
|
74
|
+
media_type: media_type_or_converter, converter: block
|
75
|
+
)
|
76
|
+
)
|
77
|
+
else
|
78
|
+
raise "must pass a String and block or a Modern::Descriptor::Converters::Input::Base."
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear_input_converters!
|
83
|
+
@settings = @settings.copy(input_converters: [])
|
84
|
+
end
|
85
|
+
|
86
|
+
def output_converter(media_type_or_converter, &block)
|
87
|
+
if media_type_or_converter.is_a?(Modern::Descriptor::Converters::Output::Base)
|
88
|
+
@settings = @settings.copy(output_converters: @settings.output_converters + [media_type_or_converter])
|
89
|
+
elsif media_type_or_converter.is_a?(String) && !block.nil?
|
90
|
+
output_converter(
|
91
|
+
Modern::Descriptor::Converters::Output::Base.new(
|
92
|
+
media_type: media_type_or_converter, converter: block
|
93
|
+
)
|
94
|
+
)
|
95
|
+
else
|
96
|
+
raise "must pass a String and block or a Modern::Descriptor::Converters::Output::Base."
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def clear_output_converters!
|
101
|
+
@settings = @settings.copy(output_converters: [])
|
102
|
+
end
|
103
|
+
|
104
|
+
def route(id, http_method, path = nil, &block)
|
105
|
+
route = RouteBuilder.evaluate(id, http_method, path, @settings.dup, &block)
|
106
|
+
@descriptor = @descriptor.copy(routes: @descriptor.routes + [route])
|
107
|
+
end
|
108
|
+
|
109
|
+
def get(id, path = nil, &block)
|
110
|
+
route(id, :get, path, &block)
|
111
|
+
end
|
112
|
+
|
113
|
+
def post(id, path = nil, &block)
|
114
|
+
route(id, :post, path, &block)
|
115
|
+
end
|
116
|
+
|
117
|
+
def put(id, path = nil, &block)
|
118
|
+
route(id, :put, path, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def delete(id, path = nil, &block)
|
122
|
+
route(id, :delete, path, &block)
|
123
|
+
end
|
124
|
+
|
125
|
+
def patch(id, path = nil, &block)
|
126
|
+
route(id, :patch, path, &block)
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.evaluate(descriptor, settings, &block)
|
130
|
+
scope = Scope.new(descriptor, settings)
|
131
|
+
scope.instance_exec(&block)
|
132
|
+
|
133
|
+
scope.descriptor
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def _scope(new_settings = {}, &block)
|
139
|
+
ret = Scope.evaluate(descriptor, @settings.copy(new_settings), &block)
|
140
|
+
ret
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/struct'
|
4
|
+
|
5
|
+
require 'modern/capsule'
|
6
|
+
require 'modern/descriptor'
|
7
|
+
|
8
|
+
require 'deep_dup'
|
9
|
+
require 'docile'
|
10
|
+
require 'ice_nine'
|
11
|
+
|
12
|
+
module Modern
|
13
|
+
module DSL
|
14
|
+
class ScopeSettings < Modern::Struct
|
15
|
+
attribute :path_segments, Types.array_of(
|
16
|
+
Types::Strict::String.constrained(
|
17
|
+
format: %r,[^/]+,
|
18
|
+
)
|
19
|
+
)
|
20
|
+
|
21
|
+
attribute :tags, Types.array_of(Types::Strict::String)
|
22
|
+
|
23
|
+
attribute :deprecated, Types::Strict::Bool.default(false)
|
24
|
+
|
25
|
+
attribute :parameters, Types.array_of(Modern::Descriptor::Parameters::Base)
|
26
|
+
|
27
|
+
attribute :default_response, Modern::Descriptor::Response.optional.default(
|
28
|
+
Modern::Descriptor::Response.new(http_code: :default)
|
29
|
+
)
|
30
|
+
|
31
|
+
# TODO: this code gets way less gross when we get Types.Map
|
32
|
+
attribute :input_converters, Types.array_of(Modern::Descriptor::Converters::Input::Base)
|
33
|
+
attribute :output_converters, Types.array_of(Modern::Descriptor::Converters::Output::Base)
|
34
|
+
|
35
|
+
attribute :security, Types.array_of(Modern::Descriptor::Security::Base)
|
36
|
+
attribute :helpers, Types.array_of(Types.Instance(Module))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/modern/dsl.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modern/descriptor'
|
4
|
+
|
5
|
+
# TODO: make the DSL in general more efficient
|
6
|
+
# The various copies in every class in this DSL builder are probably
|
7
|
+
# unavoidable. Which is a bummer. But we could probably reduce the amount
|
8
|
+
# of code involved. I've been trying to think of a good way to make a
|
9
|
+
# generalized builder for Dry::Struct; anybody have a good idea?
|
10
|
+
# TODO: Figure out why Docile (hence removed) causes settings leaks
|
11
|
+
# For SOME awful reason, `@settings` in sub-scopes is leaking out to
|
12
|
+
# parent scopes. I have only isolated this down to Docile, as when I use
|
13
|
+
# `instance_exec` it doesn't happen.
|
14
|
+
Dir["#{__dir__}/dsl/**/*.rb"].each { |f| require_relative f }
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/errors/error"
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module Errors
|
7
|
+
class WebError < Modern::Errors::Error
|
8
|
+
def status
|
9
|
+
raise "#{self.class.name}#status must be implemented."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class BadRequestError < WebError
|
14
|
+
def initialize(msg = "Bad request")
|
15
|
+
super(msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def status
|
19
|
+
400
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class UnauthorizedError < WebError
|
24
|
+
def initialize(msg = "Unauthorized")
|
25
|
+
super(msg)
|
26
|
+
end
|
27
|
+
|
28
|
+
def status
|
29
|
+
401
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ForbiddenError < WebError
|
34
|
+
def initialize(msg = "Forbidden")
|
35
|
+
super(msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
def status
|
39
|
+
403
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class NotFoundError < WebError
|
44
|
+
def initialize(msg = "Not found")
|
45
|
+
super(msg)
|
46
|
+
end
|
47
|
+
|
48
|
+
def status
|
49
|
+
404
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class NotAcceptableError < WebError
|
54
|
+
def initialize(msg = "Not acceptable (no servable content types in Accept header)")
|
55
|
+
super(msg)
|
56
|
+
end
|
57
|
+
|
58
|
+
def status
|
59
|
+
406
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class UnsupportedMediaTypeError < WebError
|
64
|
+
def initialize(msg = "Unrecognized request Content-Type.")
|
65
|
+
super(msg)
|
66
|
+
end
|
67
|
+
|
68
|
+
def status
|
69
|
+
415
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class UnprocessableEntity < WebError
|
74
|
+
def initialize(msg = "Recognized content-type of body, but could not parse it.")
|
75
|
+
super(msg)
|
76
|
+
end
|
77
|
+
|
78
|
+
def status
|
79
|
+
422
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modern
|
4
|
+
# rubocop:disable Lint/InheritException
|
5
|
+
class Redirect < Exception
|
6
|
+
attr_reader :redirect_to
|
7
|
+
|
8
|
+
def initialize(redirect_to)
|
9
|
+
raise "Redirects require a target." if redirect_to.nil?
|
10
|
+
@redirect_to = redirect_to
|
11
|
+
end
|
12
|
+
|
13
|
+
def status
|
14
|
+
raise "#{self.class.name}#status must be implemented."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
# rubocop:enable Lint/InheritException
|
18
|
+
|
19
|
+
class PermanentRedirect < Redirect
|
20
|
+
def status
|
21
|
+
308
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class TemporaryRedirect < Redirect
|
26
|
+
def status
|
27
|
+
307
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Modern
|
7
|
+
class Request < Rack::Request
|
8
|
+
# rubocop:disable Style/MutableConstant
|
9
|
+
LOCAL_REQUEST_STORE = {}
|
10
|
+
# rubocop:enable Style/MutableConstant
|
11
|
+
|
12
|
+
attr_reader :logger
|
13
|
+
|
14
|
+
def initialize(env, logger)
|
15
|
+
super(env)
|
16
|
+
|
17
|
+
env["HTTP_X_REQUEST_ID"] ||= SecureRandom.uuid
|
18
|
+
|
19
|
+
@logger = logger.child(request_id: request_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def request_id
|
23
|
+
env["HTTP_X_REQUEST_ID"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def local_store
|
27
|
+
LOCAL_REQUEST_STORE[request_id] ||= {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def cleanup
|
31
|
+
LOCAL_REQUEST_STORE.delete(request_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Modern
|
8
|
+
class Response < Rack::Response
|
9
|
+
attr_reader :request
|
10
|
+
attr_reader :bypass
|
11
|
+
|
12
|
+
def initialize(request, body = [], status = 200, header = {})
|
13
|
+
super(body, status, header)
|
14
|
+
|
15
|
+
@request = request
|
16
|
+
@bypass = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def bypass!
|
20
|
+
@bypass = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def json(object, pretty: false)
|
24
|
+
headers["Content-Type"] = "application/json"
|
25
|
+
|
26
|
+
if pretty
|
27
|
+
write(JSON.pretty_generate(object))
|
28
|
+
else
|
29
|
+
write(JSON.generate(object))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def text(object)
|
34
|
+
headers["Content-Type"] = "text/plain"
|
35
|
+
|
36
|
+
write(object.to_s)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/struct"
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
# The default services catalogue for a Modern app, and one that can be
|
7
|
+
# extended by a consuming application to add additional services. Mixins
|
8
|
+
# and multiple services from multiple packages can be done with `dry-struct`
|
9
|
+
# but looks a little bizarre:
|
10
|
+
#
|
11
|
+
# https://discourse.dry-rb.org/t/dry-struct-reusing-a-set-of-common-attributes/315/3
|
12
|
+
class Services < Modern::Struct
|
13
|
+
LoggerType = Types.Instance(Ougai::Logger) | Types.Instance(Ougai::ChildLogger)
|
14
|
+
|
15
|
+
attribute :base_logger, (LoggerType.default { Ougai::Logger.new($stderr) })
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deep_merge/rails_compat'
|
4
|
+
require 'dry/struct'
|
5
|
+
|
6
|
+
require 'modern/types'
|
7
|
+
|
8
|
+
module Modern
|
9
|
+
class Struct < Dry::Struct
|
10
|
+
module Copy
|
11
|
+
# This implementation is necessary because the "fast" way (hash, merge, recreate)
|
12
|
+
# WILL EAT YOUR TYPE DATA. This is the only way I can find to copy-but-change an
|
13
|
+
# object that doesn't.
|
14
|
+
#
|
15
|
+
# Computers are bad.
|
16
|
+
def copy(fields = {})
|
17
|
+
self.class[self.class.attribute_names.map { |n| [n, self[n]] }.to_h.merge(fields)]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
constructor_type :strict_with_defaults
|
22
|
+
|
23
|
+
include Copy
|
24
|
+
end
|
25
|
+
end
|
data/lib/modern/types.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/types'
|
4
|
+
require 'dry/struct'
|
5
|
+
|
6
|
+
require 'ice_nine'
|
7
|
+
|
8
|
+
module Modern
|
9
|
+
module Types
|
10
|
+
include Dry::Types.module
|
11
|
+
|
12
|
+
# rubocop:disable Style/MutableConstant
|
13
|
+
# This is left unfrozen so as to allow additional verbs to be added
|
14
|
+
# in the future. Should be rare, but I've seen it done...
|
15
|
+
HTTP_METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE]
|
16
|
+
# rubocop:enable Style/MutableConstant
|
17
|
+
|
18
|
+
Type = Instance(Dry::Types::Type)
|
19
|
+
Struct = Instance(Dry::Struct)
|
20
|
+
|
21
|
+
HttpMethod = Types::Coercible::String.enum(*HTTP_METHODS)
|
22
|
+
HttpPath = Types::Strict::String.constrained(
|
23
|
+
format: %r,/.*,
|
24
|
+
)
|
25
|
+
|
26
|
+
MIMEType = Types::Strict::String.constrained(
|
27
|
+
format: %r,\w+/[-.\w]+(?:\+[-.\w]+)?,
|
28
|
+
)
|
29
|
+
|
30
|
+
RouteAction = Instance(Proc)
|
31
|
+
SecurityAction = Instance(Proc)
|
32
|
+
|
33
|
+
ParameterStyle = Types::Coercible::String.enum(:matrix, :label, :form,
|
34
|
+
:simple, :space_delimited,
|
35
|
+
:pipe_delimited, :deep_object)
|
36
|
+
|
37
|
+
def self.array_of(type)
|
38
|
+
Modern::Types::Strict::Array.of(type).default([])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Modern
|
4
|
+
module Util
|
5
|
+
module HeaderParsing
|
6
|
+
def self.parse_accept_header(value)
|
7
|
+
# TODO: this is probably more garbage creation than necessary.
|
8
|
+
# TODO: may poorly prioritize specificity, i.e. `text/*` over `*/*`
|
9
|
+
# TODO: this doesn't support `;level=`, but should we bother?
|
10
|
+
value.split(",").map do |type_declaration|
|
11
|
+
tuple = type_declaration.strip.split(";q=")
|
12
|
+
tuple[1] = tuple[1]&.to_f || 1.0
|
13
|
+
|
14
|
+
tuple
|
15
|
+
end.sort do |a, b|
|
16
|
+
comp = a.last <=> b.last
|
17
|
+
|
18
|
+
if comp != 0
|
19
|
+
comp
|
20
|
+
else
|
21
|
+
-(a.first <=> b.first)
|
22
|
+
end
|
23
|
+
end.map(&:first)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "modern/errors/setup_errors"
|
4
|
+
|
5
|
+
module Modern
|
6
|
+
module Util
|
7
|
+
class TrieNode
|
8
|
+
attr_reader :parent
|
9
|
+
attr_reader :path
|
10
|
+
attr_accessor :value
|
11
|
+
|
12
|
+
attr_reader :children
|
13
|
+
|
14
|
+
def initialize(path = [])
|
15
|
+
@path = path
|
16
|
+
@children = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(key, value, raise_if_present: false)
|
20
|
+
key = [key].flatten
|
21
|
+
|
22
|
+
if key.empty?
|
23
|
+
if @value
|
24
|
+
raise Modern::Errors::RoutingError, "Existing value at #{path.inspect}: #{@value}" \
|
25
|
+
if raise_if_present
|
26
|
+
end
|
27
|
+
|
28
|
+
@value = value
|
29
|
+
else
|
30
|
+
child_name = key.first
|
31
|
+
@children[child_name] ||= TrieNode.new(path + [child_name])
|
32
|
+
|
33
|
+
@children[child_name].add(key[1..-1], value, raise_if_present: raise_if_present)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](child_name)
|
38
|
+
@children[child_name] || @children[:templated]
|
39
|
+
end
|
40
|
+
|
41
|
+
def get(key = [])
|
42
|
+
key = [key].flatten
|
43
|
+
|
44
|
+
node = self
|
45
|
+
until key.empty? || node.nil?
|
46
|
+
node = node[key.shift]
|
47
|
+
end
|
48
|
+
|
49
|
+
node&.value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|