modern 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|