kangaru 0.1.0

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