hanami 2.0.0.alpha1 → 2.0.0.alpha5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +306 -5
  3. data/FEATURES.md +9 -1
  4. data/LICENSE.md +1 -1
  5. data/README.md +9 -6
  6. data/hanami.gemspec +12 -11
  7. data/lib/hanami/application/autoloader/inflector_adapter.rb +22 -0
  8. data/lib/hanami/application/container/boot/inflector.rb +7 -0
  9. data/lib/hanami/application/container/boot/logger.rb +7 -0
  10. data/lib/hanami/application/container/boot/rack_logger.rb +19 -0
  11. data/lib/hanami/application/container/boot/rack_monitor.rb +12 -0
  12. data/lib/hanami/application/container/boot/routes_helper.rb +9 -0
  13. data/lib/hanami/application/container/boot/settings.rb +7 -0
  14. data/lib/hanami/application/router.rb +59 -0
  15. data/lib/hanami/application/routes.rb +55 -0
  16. data/lib/hanami/application/routes_helper.rb +34 -0
  17. data/lib/hanami/application/routing/middleware/stack.rb +89 -0
  18. data/lib/hanami/application/routing/resolver/node.rb +50 -0
  19. data/lib/hanami/application/routing/resolver/trie.rb +59 -0
  20. data/lib/hanami/application/routing/resolver.rb +87 -0
  21. data/lib/hanami/application/routing/router.rb +36 -0
  22. data/lib/hanami/application/settings/dotenv_store.rb +60 -0
  23. data/lib/hanami/application/settings.rb +93 -0
  24. data/lib/hanami/application.rb +330 -34
  25. data/lib/hanami/assets/application_configuration.rb +63 -0
  26. data/lib/hanami/assets/configuration.rb +54 -0
  27. data/lib/hanami/boot/source_dirs.rb +44 -0
  28. data/lib/hanami/boot.rb +1 -2
  29. data/lib/hanami/cli/application/cli.rb +40 -0
  30. data/lib/hanami/cli/application/command.rb +47 -0
  31. data/lib/hanami/cli/application/commands/console.rb +81 -0
  32. data/lib/hanami/cli/application/commands.rb +16 -0
  33. data/lib/hanami/cli/base_command.rb +48 -0
  34. data/lib/hanami/cli/commands/command.rb +4 -4
  35. data/lib/hanami/cli/commands.rb +3 -2
  36. data/lib/hanami/configuration/logger.rb +84 -0
  37. data/lib/hanami/configuration/middleware.rb +4 -4
  38. data/lib/hanami/configuration/null_configuration.rb +14 -0
  39. data/lib/hanami/configuration/router.rb +52 -0
  40. data/lib/hanami/configuration/sessions.rb +5 -5
  41. data/lib/hanami/configuration/source_dirs.rb +42 -0
  42. data/lib/hanami/configuration.rb +122 -131
  43. data/lib/hanami/init.rb +5 -0
  44. data/lib/hanami/setup.rb +9 -0
  45. data/lib/hanami/slice.rb +189 -0
  46. data/lib/hanami/version.rb +1 -1
  47. data/lib/hanami/web/rack_logger.rb +96 -0
  48. data/lib/hanami.rb +17 -30
  49. metadata +116 -50
  50. data/bin/hanami +0 -8
  51. data/lib/hanami/configuration/cookies.rb +0 -24
  52. data/lib/hanami/configuration/security.rb +0 -141
  53. data/lib/hanami/container.rb +0 -107
  54. data/lib/hanami/frameworks.rb +0 -28
  55. data/lib/hanami/routes.rb +0 -31
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Application
5
+ # Application routes
6
+ #
7
+ # Users are expected to inherit from this class to define their application
8
+ # routes.
9
+ #
10
+ # @example
11
+ # # config/routes.rb
12
+ # # frozen_string_literal: true
13
+ #
14
+ # require "hanami/application/routes"
15
+ #
16
+ # module MyApp
17
+ # class Routes < Hanami::Application::Routes
18
+ # define do
19
+ # slice :main, at: "/" do
20
+ # root to: "home.show"
21
+ # end
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # See {Hanami::Application::Router} for the syntax allowed within the
27
+ # `define` block.
28
+ #
29
+ # @see Hanami::Application::Router
30
+ # @since 2.0.0
31
+ class Routes
32
+ # Defines application routes
33
+ #
34
+ # @yield DSL syntax to define application routes executed in the context
35
+ # of {Hanami::Application::Router}
36
+ #
37
+ # @return [Proc]
38
+ def self.define(&block)
39
+ @_routes = block
40
+ end
41
+
42
+ # @api private
43
+ def self.routes
44
+ @_routes || raise(<<~MSG)
45
+ Routes need to be defined before being able to fetch them. E.g.,
46
+ define do
47
+ slice :main, at: "/" do
48
+ root to: "home.show"
49
+ end
50
+ end
51
+ MSG
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Application
5
+ # Hanami application routes helpers
6
+ #
7
+ # An instance of this class gets registered in the container
8
+ # (`routes_helper` key) once the Hanami application is booted. You can use
9
+ # it to get the route helpers for your application.
10
+ #
11
+ # @example
12
+ # MyApp::Application["routes_helper"].path(:root) # => "/"
13
+ #
14
+ # @see Hanami::Router::UrlHelpers
15
+ # @since 2.0.0
16
+ class RoutesHelper
17
+ # @since 2.0.0
18
+ # @api private
19
+ def initialize(router)
20
+ @router = router
21
+ end
22
+
23
+ # @see Hanami::Router::UrlHelpers#path
24
+ def path(*args, **kwargs, &block)
25
+ @router.path(*args, **kwargs, &block)
26
+ end
27
+
28
+ # @see Hanami::Router::UrlHelpers#url
29
+ def url(*args, **kwargs, &block)
30
+ @router.url(*args, **kwargs, &block)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/builder"
4
+
5
+ module Hanami
6
+ class Application
7
+ module Routing
8
+ # Hanami::Applicatione::Router middleware stack
9
+ #
10
+ # @since 2.0.0
11
+ # @api private
12
+ module Middleware
13
+ # Middleware stack
14
+ #
15
+ # @since 2.0.0
16
+ # @api private
17
+ class Stack
18
+ # @since 2.0.0
19
+ # @api private
20
+ ROOT_PREFIX = "/"
21
+ private_constant :ROOT_PREFIX
22
+
23
+ # @since 2.0.0
24
+ # @api private
25
+ def initialize
26
+ @prefix = ROOT_PREFIX
27
+ @stack = Hash.new { |hash, key| hash[key] = [] }
28
+ end
29
+
30
+ # @since 2.0.0
31
+ # @api private
32
+ def use(middleware, *args, &blk)
33
+ @stack[@prefix].push([middleware, args, blk])
34
+ end
35
+
36
+ # @since 2.0.0
37
+ # @api private
38
+ def with(path)
39
+ prefix = @prefix
40
+ @prefix = path
41
+ yield
42
+ ensure
43
+ @prefix = prefix
44
+ end
45
+
46
+ # @since 2.0.0
47
+ # @api private
48
+ def to_rack_app(app) # rubocop:disable Metrics/MethodLength
49
+ s = self
50
+
51
+ Rack::Builder.new do
52
+ s.each do |prefix, stack|
53
+ s.mapped(self, prefix) do
54
+ stack.each do |middleware, args, blk|
55
+ use(middleware, *args, &blk)
56
+ end
57
+ end
58
+
59
+ run app
60
+ end
61
+ end.to_app
62
+ end
63
+
64
+ # @since 2.0.0
65
+ # @api private
66
+ def empty?
67
+ @stack.empty?
68
+ end
69
+
70
+ # @since 2.0.0
71
+ # @api private
72
+ def each(&blk)
73
+ @stack.each(&blk)
74
+ end
75
+
76
+ # @since 2.0.0
77
+ # @api private
78
+ def mapped(builder, prefix, &blk)
79
+ if prefix == ROOT_PREFIX
80
+ builder.instance_eval(&blk)
81
+ else
82
+ builder.map(prefix, &blk)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Application
5
+ module Routing
6
+ class Resolver
7
+ # Endpoint resolver node to register slices in a tree
8
+ #
9
+ # @api private
10
+ # @since 2.0.0
11
+ class Node
12
+ # @api private
13
+ # @since 2.0.0
14
+ attr_reader :slice
15
+
16
+ # @api private
17
+ # @since 2.0.0
18
+ def initialize
19
+ @slice = nil
20
+ @children = {}
21
+ end
22
+
23
+ # @api private
24
+ # @since 2.0.0
25
+ def put(segment)
26
+ @children[segment] ||= self.class.new
27
+ end
28
+
29
+ # @api private
30
+ # @since 2.0.0
31
+ def get(segment)
32
+ @children.fetch(segment) { self if leaf? }
33
+ end
34
+
35
+ # @api private
36
+ # @since 2.0.0
37
+ def leaf!(slice)
38
+ @slice = slice
39
+ end
40
+
41
+ # @api private
42
+ # @since 2.0.0
43
+ def leaf?
44
+ @slice
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/application/routing/resolver/node"
4
+
5
+ module Hanami
6
+ class Application
7
+ module Routing
8
+ class Resolver
9
+ # Endpoint resolver trie to register slices
10
+ #
11
+ # @api private
12
+ # @since 2.0.0
13
+ class Trie
14
+ # @api private
15
+ # @since 2.0.0
16
+ def initialize
17
+ @root = Node.new
18
+ end
19
+
20
+ # @api private
21
+ # @since 2.0.0
22
+ def add(path, name)
23
+ node = @root
24
+ for_each_segment(path) do |segment|
25
+ node = node.put(segment)
26
+ end
27
+
28
+ node.leaf!(name)
29
+ end
30
+
31
+ # @api private
32
+ # @since 2.0.0
33
+ def find(path)
34
+ node = @root
35
+
36
+ for_each_segment(path) do |segment|
37
+ break unless node
38
+
39
+ node = node.get(segment)
40
+ end
41
+
42
+ return node.slice if node&.leaf?
43
+
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ # @api private
50
+ # @since 2.0.0
51
+ def for_each_segment(path, &blk)
52
+ _, *segments = path.split(/\//)
53
+ segments.each(&blk)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Application
5
+ module Routing
6
+ # Hanami application router endpoint resolver
7
+ #
8
+ # @since 2.0.0
9
+ class Resolver
10
+ ENDPOINT_KEY_NAMESPACE = "actions"
11
+
12
+ require_relative "resolver/trie"
13
+
14
+ # @since 2.0.0
15
+ class NotCallableEndpointError < StandardError
16
+ def initialize(endpoint)
17
+ super("#{endpoint.inspect} is not compatible with Rack. Please make sure it implements #call.")
18
+ end
19
+ end
20
+
21
+ # @api private
22
+ # @since 2.0.0
23
+ def initialize(slices:, inflector:)
24
+ @slices = slices
25
+ @inflector = inflector
26
+ @slice_registry = Trie.new
27
+ end
28
+
29
+ # @api private
30
+ # @since 2.0.0
31
+ #
32
+ # rubocop:disable Metrics/MethodLength
33
+ def call(path, identifier)
34
+ endpoint =
35
+ case identifier
36
+ when String
37
+ resolve_string_identifier(path, identifier)
38
+ when Class
39
+ identifier.respond_to?(:call) ? identifier : identifier.new
40
+ else
41
+ identifier
42
+ end
43
+
44
+ unless endpoint.respond_to?(:call) # rubocop:disable Style/IfUnlessModifier
45
+ raise NotCallableEndpointError.new(endpoint)
46
+ end
47
+
48
+ endpoint
49
+ end
50
+ # rubocop:enable Metrics/MethodLength
51
+
52
+ # @api private
53
+ # @since 2.0.0
54
+ def register_slice_at_path(name, path)
55
+ slice_registry.add(path, name)
56
+ end
57
+
58
+ private
59
+
60
+ # @api private
61
+ # @since 2.0.0
62
+ attr_reader :slices
63
+
64
+ # @api private
65
+ # @since 2.0.0
66
+ attr_reader :inflector
67
+
68
+ # @api private
69
+ # @since 2.0.0
70
+ attr_reader :slice_registry
71
+
72
+ # @api private
73
+ # @since 2.0.0
74
+ def resolve_string_identifier(path, identifier)
75
+ slice_name = slice_registry.find(path) or raise "missing slice for #{path.inspect} (#{identifier.inspect})"
76
+ slice = slices[slice_name]
77
+ endpoint_key = "#{ENDPOINT_KEY_NAMESPACE}.#{identifier}"
78
+
79
+ # Lazily resolve endpoint from the slice to reduce router initialization time,
80
+ # and break potential endless loops from the resolved endpoint itself requiring
81
+ # access to router-related concerns
82
+ -> (*args) { slice[endpoint_key].call(*args) }
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,36 @@
1
+ # # frozen_string_literal: true
2
+
3
+ # require "hanami/application/router"
4
+
5
+ # Hanami.application.register_bootable :router do
6
+ # start do
7
+ # configuration = Hanami.application.configuration
8
+
9
+ # routes = begin
10
+ # require File.join(configuration.root, configuration.router.routes_path)
11
+ # routes_class = Hanami.application.send(:autodiscover_application_constant, configuration.router.routes_class_name) # WIP private
12
+ # routes_class.routes
13
+ # rescue LoadError
14
+ # proc {}
15
+ # end
16
+
17
+ # resolver = configuration.router.resolver.new(
18
+ # slices: Hanami.application.slices,
19
+ # inflector: Hanami.application.inflector # TODO: use container[:inflector]?
20
+ # )
21
+
22
+ # router = Hanami::Application::Router.new(
23
+ # routes: routes,
24
+ # resolver: resolver,
25
+ # **configuration.router.options,
26
+ # ) do
27
+ # use Hanami.application[:rack_monitor]
28
+
29
+ # Hanami.application.config.for_each_middleware do |m, *args, &block|
30
+ # use(m, *args, &block)
31
+ # end
32
+ # end
33
+
34
+ # register :router, router
35
+ # end
36
+ # end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/constants"
4
+
5
+ module Hanami
6
+ class Application
7
+ class Settings
8
+ # Default application settings store.
9
+ #
10
+ # Uses [dotenv](https://github.com/bkeepers/dotenv) (if available) to load
11
+ # .env files and then loads settings from ENV. For a given `HANAMI_ENV`
12
+ # environment, the following `.env` files are looked up in the following order:
13
+ #
14
+ # - .env.{environment}.local
15
+ # - .env.local (except if the environment is `test`)
16
+ # - .env.{environment}
17
+ # - .env
18
+ #
19
+ # @since 2.0.0
20
+ # @api private
21
+ class DotenvStore
22
+ Undefined = Dry::Core::Constants::Undefined
23
+
24
+ attr_reader :store,
25
+ :hanami_env
26
+
27
+ def initialize(store: ENV, hanami_env: Hanami.env)
28
+ @store = store
29
+ @hanami_env = hanami_env
30
+ end
31
+
32
+ def fetch(name, default_value = Undefined, &block)
33
+ name = name.to_s.upcase
34
+ args = (default_value == Undefined) ? [name] : [name, default_value]
35
+
36
+ store.fetch(*args, &block)
37
+ end
38
+
39
+ def with_dotenv_loaded
40
+ require "dotenv"
41
+ Dotenv.load(*dotenv_files) if defined?(Dotenv)
42
+ self
43
+ rescue LoadError
44
+ self
45
+ end
46
+
47
+ private
48
+
49
+ def dotenv_files
50
+ [
51
+ ".env.#{hanami_env}.local",
52
+ (".env.local" unless hanami_env == :test),
53
+ ".env.#{hanami_env}",
54
+ ".env"
55
+ ].compact
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "dry/core/constants"
5
+
6
+ module Hanami
7
+ class Application
8
+ # Application settings
9
+ #
10
+ # Users are expected to inherit from this class to define their application
11
+ # settings.
12
+ #
13
+ # @example
14
+ # # config/settings.rb
15
+ # # frozen_string_literal: true
16
+ #
17
+ # require "hanami/application/settings"
18
+ # require "my_app/types"
19
+ #
20
+ # module MyApp
21
+ # class Settings < Hanami::Application::Settings
22
+ # setting :database_url
23
+ # setting :feature_flag, default: false, constructor: Types::Params::Bool
24
+ # end
25
+ # end
26
+ #
27
+ # Settings are defined with
28
+ # [dry-configurable](https://dry-rb.org/gems/dry-configurable/), so you can
29
+ # take a look there to see the supported syntax.
30
+ #
31
+ # Users work with an instance of this class made available within the
32
+ # `settings` key in the container. The instance gets its settings populated
33
+ # from a configurable store, which defaults to
34
+ # {Hanami::Application::Settings::DotenvStore}.
35
+ #
36
+ # A different store can be set through the `settings_store` Hanami
37
+ # configuration option. All it needs to do is implementing a `#fetch` method
38
+ # with the same signature as `Hash#fetch`.
39
+ #
40
+ # @see Hanami::Application::Settings::DotenvStore
41
+ # @since 2.0.0
42
+ class Settings
43
+ # Exception for errors in the definition of settings.
44
+ #
45
+ # Its message collects all the individual errors that can be raised for
46
+ # each setting.
47
+ InvalidSettingsError = Class.new(StandardError) do
48
+ def initialize(errors)
49
+ @errors = errors
50
+ end
51
+
52
+ def to_s
53
+ <<~STR.strip
54
+ Could not initialize settings. The following settings were invalid:
55
+
56
+ #{@errors.map { |setting, message| "#{setting}: #{message}" }.join("\n")}
57
+ STR
58
+ end
59
+ end
60
+
61
+ # @api private
62
+ EMPTY_STORE = Dry::Core::Constants::EMPTY_HASH
63
+
64
+ include Dry::Configurable
65
+
66
+ # @api private
67
+ def initialize(store = EMPTY_STORE)
68
+ errors = config._settings.map(&:name).reduce({}) do |errs, name|
69
+ public_send("#{name}=", store.fetch(name) { Dry::Core::Constants::Undefined })
70
+ errs
71
+ rescue => e # rubocop:disable Style/RescueStandardError
72
+ errs.merge(name => e)
73
+ end
74
+
75
+ raise InvalidSettingsError, errors if errors.any?
76
+ end
77
+
78
+ private
79
+
80
+ def method_missing(name, *args, &block)
81
+ if config.respond_to?(name)
82
+ config.send(name, *args, &block)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ def respond_to_missing?(name, _include_all = false)
89
+ config.respond_to?(name) || super
90
+ end
91
+ end
92
+ end
93
+ end