kangaru 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +75 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +14 -0
  7. data/Gemfile.lock +139 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +49 -0
  10. data/Rakefile +10 -0
  11. data/Steepfile +12 -0
  12. data/kangaru.gemspec +27 -0
  13. data/lib/kangaru/application.rb +73 -0
  14. data/lib/kangaru/argument_parser.rb +34 -0
  15. data/lib/kangaru/components/component.rb +19 -0
  16. data/lib/kangaru/concerns/attributes_concern.rb +31 -0
  17. data/lib/kangaru/concerns/concern.rb +33 -0
  18. data/lib/kangaru/concerns/configurable.rb +22 -0
  19. data/lib/kangaru/concerns/validatable.rb +46 -0
  20. data/lib/kangaru/config.rb +45 -0
  21. data/lib/kangaru/configurators/application_configurator.rb +7 -0
  22. data/lib/kangaru/configurators/configurator.rb +22 -0
  23. data/lib/kangaru/configurators/database_configurator.rb +7 -0
  24. data/lib/kangaru/configurators/external_configurator.rb +6 -0
  25. data/lib/kangaru/configurators/open_configurator.rb +33 -0
  26. data/lib/kangaru/configurators/request_configurator.rb +7 -0
  27. data/lib/kangaru/configurators.rb +12 -0
  28. data/lib/kangaru/controller.rb +53 -0
  29. data/lib/kangaru/database.rb +54 -0
  30. data/lib/kangaru/inflectors/class_inflector.rb +13 -0
  31. data/lib/kangaru/inflectors/constant_inflector.rb +13 -0
  32. data/lib/kangaru/inflectors/constantiser.rb +18 -0
  33. data/lib/kangaru/inflectors/human_inflector.rb +15 -0
  34. data/lib/kangaru/inflectors/inflector.rb +98 -0
  35. data/lib/kangaru/inflectors/inflector_macros.rb +33 -0
  36. data/lib/kangaru/inflectors/path_inflector.rb +21 -0
  37. data/lib/kangaru/inflectors/screaming_snakecase_inflector.rb +11 -0
  38. data/lib/kangaru/inflectors/snakecase_inflector.rb +11 -0
  39. data/lib/kangaru/inflectors/tokeniser.rb +20 -0
  40. data/lib/kangaru/initialiser.rb +12 -0
  41. data/lib/kangaru/initialisers/rspec.rb +11 -0
  42. data/lib/kangaru/injected_methods.rb +23 -0
  43. data/lib/kangaru/model.rb +7 -0
  44. data/lib/kangaru/patches/constantise.rb +11 -0
  45. data/lib/kangaru/patches/inflections.rb +23 -0
  46. data/lib/kangaru/patches/source.rb +11 -0
  47. data/lib/kangaru/patches/symboliser.rb +29 -0
  48. data/lib/kangaru/path_parser.rb +35 -0
  49. data/lib/kangaru/paths.rb +93 -0
  50. data/lib/kangaru/renderer.rb +15 -0
  51. data/lib/kangaru/request.rb +40 -0
  52. data/lib/kangaru/request_builder.rb +33 -0
  53. data/lib/kangaru/router.rb +38 -0
  54. data/lib/kangaru/validation/attribute_validator.rb +32 -0
  55. data/lib/kangaru/validation/error.rb +32 -0
  56. data/lib/kangaru/validators/required_validator.rb +17 -0
  57. data/lib/kangaru/validators/validator.rb +22 -0
  58. data/lib/kangaru/version.rb +3 -0
  59. data/lib/kangaru.rb +58 -0
  60. data/rbs_collection.lock.yaml +83 -0
  61. data/rbs_collection.yaml +24 -0
  62. metadata +173 -0
@@ -0,0 +1,22 @@
1
+ module Kangaru
2
+ module Configurators
3
+ class Configurator
4
+ include Concerns::AttributesConcern
5
+
6
+ using Patches::Inflections
7
+
8
+ def self.key
9
+ to_s.gsub(/^.*::(?!.*::)/, "")
10
+ .delete_suffix("Configurator")
11
+ .to_snakecase
12
+ .to_sym
13
+ end
14
+
15
+ def serialise
16
+ self.class.attributes.to_h do |setting|
17
+ [setting, send(setting)]
18
+ end.compact
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Kangaru
2
+ module Configurators
3
+ class DatabaseConfigurator < Configurator
4
+ attr_accessor :adaptor, :path, :migration_path
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Kangaru
2
+ module Configurators
3
+ class ExternalConfigurator < OpenConfigurator
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,33 @@
1
+ # Similar to a standard configurator, except on initialisation, it will set
2
+ # accessors for every attribute specified. This means that the super call will
3
+ # lead to each value being set as if the accessor was defined in the class.
4
+ module Kangaru
5
+ module Configurators
6
+ class OpenConfigurator < Configurator
7
+ using Patches::Symboliser
8
+
9
+ def initialize(**)
10
+ set_accessors!(**)
11
+
12
+ super
13
+ end
14
+
15
+ # Import contents of a yaml file
16
+ def self.from_yaml_file(path)
17
+ raise "path does not exist" unless File.exist?(path)
18
+
19
+ attributes = YAML.load_file(path).symbolise
20
+
21
+ new(**attributes)
22
+ end
23
+
24
+ private
25
+
26
+ def set_accessors!(**attributes)
27
+ attributes.each_key do |key|
28
+ self.class.class_eval { attr_accessor key }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Kangaru
2
+ module Configurators
3
+ class RequestConfigurator < Configurator
4
+ attr_accessor :default_controller
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ module Kangaru
2
+ module Configurators
3
+ # These are not set as accessors by Config instances as they are abstract.
4
+ BASE_CONFIGURATORS = [Configurator, OpenConfigurator].freeze
5
+
6
+ def self.classes
7
+ constants.map { |constant| const_get(constant) }
8
+ .select { |constant| constant.is_a?(Class) }
9
+ .reject { |constant| BASE_CONFIGURATORS.include?(constant) }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+ module Kangaru
2
+ class Controller
3
+ extend Forwardable
4
+
5
+ using Patches::Inflections
6
+
7
+ SUFFIX = "Controller".freeze
8
+
9
+ attr_reader :request
10
+
11
+ def initialize(request)
12
+ @request = request
13
+ end
14
+
15
+ def execute
16
+ public_send(request.action)
17
+
18
+ renderer_for(request.action.to_s).render(binding)
19
+ end
20
+
21
+ # Returns the partial path for the controller based on the class name.
22
+ # The first module namespace is removed as this is either Kangaru or the
23
+ # target gem namespace. Used to infer the location of view files.
24
+ def self.path
25
+ name&.delete_suffix(SUFFIX)&.gsub(/^.*?::/, "")&.to_snakecase || raise
26
+ end
27
+
28
+ def_delegators :request, :params
29
+
30
+ private
31
+
32
+ def view_path(file)
33
+ Kangaru.application.view_path(self.class.path, file)
34
+ end
35
+
36
+ def renderer_for(file)
37
+ Renderer.new(view_path(file))
38
+ end
39
+
40
+ # The binding passed to the renderer is not scoped to the application
41
+ # namespace, and as such, all application classes must be prefixed with the
42
+ # namespace module. This change emulates the binding being created from
43
+ # within the application namespace by delegating const lookups to said
44
+ # namespace if the constant is not in scope from the current class.
45
+ def self.const_missing(const)
46
+ return super unless Kangaru.application.namespace.const_defined?(const)
47
+
48
+ Kangaru.application.namespace.const_get(const)
49
+ end
50
+
51
+ private_class_method :const_missing
52
+ end
53
+ end
@@ -0,0 +1,54 @@
1
+ module Kangaru
2
+ class Database
3
+ extend Forwardable
4
+
5
+ include Concerns::AttributesConcern
6
+
7
+ PLUGINS = %i[
8
+ enum
9
+ timestamps
10
+ ].freeze
11
+
12
+ attr_accessor :adaptor, :path, :migration_path
13
+
14
+ attr_reader :handler
15
+
16
+ def setup!
17
+ raise "adaptor can't be blank" if adaptor.nil?
18
+
19
+ @handler = case adaptor
20
+ when :sqlite then setup_sqlite!
21
+ else raise "invalid adaptor '#{adaptor}'"
22
+ end
23
+ end
24
+
25
+ def migrate!
26
+ return unless handler
27
+ return unless migrations_exist?
28
+
29
+ Sequel.extension(:migration)
30
+
31
+ Sequel::Migrator.run(handler, migration_path)
32
+ end
33
+
34
+ def_delegators :handler, :tables
35
+
36
+ private
37
+
38
+ def migrations_exist?
39
+ return false if migration_path.nil?
40
+
41
+ Dir.exist?(migration_path) && !Dir.empty?(migration_path)
42
+ end
43
+
44
+ def setup_sqlite!
45
+ raise "path can't be blank" if path.nil?
46
+
47
+ FileUtils.mkdir_p(File.dirname(path))
48
+
49
+ Sequel.sqlite(path).tap do
50
+ PLUGINS.each { |plugin| Sequel::Model.plugin(plugin) }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class ClassInflector < Inflector
4
+ filter_input_with(/\.[a-z]+$/)
5
+
6
+ transform_tokens_with :capitalize
7
+
8
+ join_tokens_with ""
9
+
10
+ join_groups_with "::"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class ConstantInflector < ClassInflector
4
+ LAST_WORD = /(::)?(?!.*::)(.*)$/
5
+
6
+ def inflect
7
+ super.gsub(LAST_WORD) do |last_word|
8
+ ScreamingSnakecaseInflector.inflect(last_word)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class Constantiser
4
+ using Patches::Inflections
5
+
6
+ def self.constantise(string, root: Object)
7
+ as_class = string.to_class_name
8
+ as_constant = string.to_constant_name
9
+
10
+ if root.const_defined?(as_class)
11
+ root.const_get(as_class)
12
+ elsif root.const_defined?(as_constant)
13
+ root.const_get(as_constant)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class HumanInflector < Inflector
4
+ transform_tokens_with :downcase
5
+
6
+ join_tokens_with " "
7
+
8
+ join_groups_with " "
9
+
10
+ post_process_with do |output|
11
+ output.strip.capitalize
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,98 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class Inflector
4
+ extend InflectorMacros
5
+
6
+ DEFAULT_GROUP_JOINER = "/".freeze
7
+
8
+ attr_reader :string
9
+
10
+ def initialize(string)
11
+ @string = filter_input(string)
12
+ end
13
+
14
+ def inflect
15
+ post_process(
16
+ join_groups(
17
+ transform_and_join_tokens(tokeniser.split)
18
+ )
19
+ )
20
+ end
21
+
22
+ def self.inflect(string)
23
+ new(string).inflect
24
+ end
25
+
26
+ private
27
+
28
+ def tokeniser
29
+ @tokeniser ||= Tokeniser.new(string)
30
+ end
31
+
32
+ def class_attribute(key)
33
+ self.class.instance_variable_get(:"@#{key}")
34
+ end
35
+
36
+ def input_filter
37
+ class_attribute(:input_filter)
38
+ end
39
+
40
+ def token_transformer
41
+ class_attribute(:token_transformer)
42
+ end
43
+
44
+ def token_joiner
45
+ class_attribute(:token_joiner)
46
+ end
47
+
48
+ def group_joiner
49
+ class_attribute(:group_joiner) || DEFAULT_GROUP_JOINER
50
+ end
51
+
52
+ def post_processor
53
+ class_attribute(:post_processor)
54
+ end
55
+
56
+ def filter_input(input)
57
+ case input_filter
58
+ when Regexp then input.gsub(input_filter, "")
59
+ else input
60
+ end
61
+ end
62
+
63
+ def transform_and_join_tokens(token_groups)
64
+ token_groups.map do |tokens|
65
+ join_tokens(
66
+ tokens.map { |token| transform_token(token) }
67
+ )
68
+ end
69
+ end
70
+
71
+ def transform_token(token)
72
+ case token_transformer
73
+ when Proc then token_transformer.call(token)
74
+ when Symbol then token.send(token_transformer)
75
+ else token
76
+ end
77
+ end
78
+
79
+ def join_tokens(tokens)
80
+ tokens.join(token_joiner)
81
+ end
82
+
83
+ def join_groups(words)
84
+ words.join(group_joiner)
85
+ end
86
+
87
+ def post_process(string)
88
+ return string if post_processor.nil?
89
+
90
+ case post_processor
91
+ when Proc then post_processor.call(string)
92
+ when Symbol then string.send(post_processor)
93
+ else string
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ module InflectorMacros
4
+ def inherited(child_class)
5
+ instance_variables.each do |rule|
6
+ value = instance_variable_get(rule)
7
+
8
+ child_class.instance_variable_set(rule, value)
9
+ end
10
+ end
11
+
12
+ def filter_input_with(pattern)
13
+ @input_filter = pattern
14
+ end
15
+
16
+ def transform_tokens_with(symbol = nil, &block)
17
+ @token_transformer = symbol || block
18
+ end
19
+
20
+ def join_tokens_with(joiner)
21
+ @token_joiner = joiner
22
+ end
23
+
24
+ def join_groups_with(joiner)
25
+ @group_joiner = joiner
26
+ end
27
+
28
+ def post_process_with(symbol = nil, &block)
29
+ @post_processor = symbol || block
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class PathInflector < Inflector
4
+ filter_input_with(/\.[a-z]+$/)
5
+
6
+ transform_tokens_with :downcase
7
+
8
+ join_tokens_with "_"
9
+
10
+ join_groups_with "/"
11
+
12
+ def inflect(with_ext: nil)
13
+ inflection = super()
14
+
15
+ return inflection unless with_ext
16
+
17
+ "#{inflection}.#{with_ext}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class ScreamingSnakecaseInflector < SnakecaseInflector
4
+ join_groups_with "::"
5
+
6
+ def inflect
7
+ super.upcase
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class SnakecaseInflector < Inflector
4
+ transform_tokens_with :downcase
5
+
6
+ join_tokens_with "_"
7
+
8
+ join_groups_with "/"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Kangaru
2
+ module Inflectors
3
+ class Tokeniser
4
+ GROUP_DELIMITER = %r{/|::}
5
+ TOKEN_DELIMITER = /[_-]+|(?=[A-Z][a-z])/
6
+
7
+ attr_reader :string
8
+
9
+ def initialize(string)
10
+ @string = string
11
+ end
12
+
13
+ def split
14
+ string.split(GROUP_DELIMITER).map do |group|
15
+ group.split(TOKEN_DELIMITER)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module Kangaru
2
+ module Initialiser
3
+ def self.extended(namespace)
4
+ source = caller[0].gsub(/:.*$/, "")
5
+
6
+ Kangaru.application = Application.new(source:, namespace:)
7
+ Kangaru.eager_load(Initialisers)
8
+
9
+ namespace.extend InjectedMethods
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module Kangaru
2
+ module Initialisers
3
+ module RSpec
4
+ if Object.const_defined?(:RSpec)
5
+ ::RSpec.configure do
6
+ Kangaru.env = :test
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module Kangaru
2
+ module InjectedMethods
3
+ def run!(*argv)
4
+ Kangaru.application.run!(*argv)
5
+ end
6
+
7
+ def config
8
+ Kangaru.application.config
9
+ end
10
+
11
+ def configure(env = nil, &)
12
+ Kangaru.application.configure(env, &)
13
+ end
14
+
15
+ def apply_config!
16
+ Kangaru.application.apply_config!
17
+ end
18
+
19
+ def database
20
+ Kangaru.application.database
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ module Kangaru
2
+ Model = Class.new(Sequel::Model) do
3
+ include Concerns::Validatable
4
+ end
5
+
6
+ Model.def_Model(self)
7
+ end
@@ -0,0 +1,11 @@
1
+ module Kangaru
2
+ module Patches
3
+ module Constantise
4
+ refine String do
5
+ def constantise(root: Object)
6
+ Inflectors::Constantiser.constantise(self, root:)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module Kangaru
2
+ module Patches
3
+ module Inflections
4
+ refine String do
5
+ def to_class_name
6
+ Inflectors::ClassInflector.inflect(self)
7
+ end
8
+
9
+ def to_constant_name
10
+ Inflectors::ConstantInflector.inflect(self)
11
+ end
12
+
13
+ def to_snakecase
14
+ Inflectors::SnakecaseInflector.inflect(self)
15
+ end
16
+
17
+ def to_humanised
18
+ Inflectors::HumanInflector.inflect(self)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module Kangaru
2
+ module Patches
3
+ module Source
4
+ refine Module do
5
+ def source
6
+ Object.const_source_location(name || raise)&.first || raise
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module Kangaru
2
+ module Patches
3
+ module Symboliser
4
+ refine Hash do
5
+ def symbolise
6
+ to_h do |key, value|
7
+ value = case value
8
+ when Array, Hash then value.symbolise
9
+ else value
10
+ end
11
+
12
+ [key.to_sym, value]
13
+ end
14
+ end
15
+ end
16
+
17
+ refine Array do
18
+ def symbolise
19
+ map do |value|
20
+ case value
21
+ when Array, Hash then value.symbolise
22
+ else value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ module Kangaru
2
+ class PathParser
3
+ attr_reader :path
4
+
5
+ def initialize(path)
6
+ @path = path
7
+ end
8
+
9
+ def controller
10
+ match(:controller)
11
+ end
12
+
13
+ def action
14
+ match(:action, cast: :to_sym)
15
+ end
16
+
17
+ def id
18
+ match(:id, cast: :to_i)
19
+ end
20
+
21
+ private
22
+
23
+ MATCHERS = {
24
+ controller: %r{/(.*)/[A-z]+(/\d+)?$},
25
+ action: %r{^.*/([A-z]+)(/\d+)?$},
26
+ id: %r{^.*/(\d+)$}
27
+ }.freeze
28
+
29
+ def match(key, cast: :to_s)
30
+ return unless (value = path.scan(MATCHERS[key]).flatten.first)
31
+
32
+ value.send(cast)
33
+ end
34
+ end
35
+ end