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
         
     |