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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 04e8f14e4f41534168d56fdc09364a9507553658887f513a1536fe71f6a9c608
4
+ data.tar.gz: 3af9ce12c3a133964cf66393f0d93339c37ae5329c3bd3ddcd65f53e1f224a28
5
+ SHA512:
6
+ metadata.gz: 6c9ee4ea2cf630d10e52d9a0a549f0ce77c5751c0469ca523baa91e15aa7eaa2e479e6120a683ad6cfb7aed8a6987cd7209d11a51a1503daa4910721718df1d8
7
+ data.tar.gz: d7c959405a39a4086409f8e1a739643502ef102b662728dc1859e9bc3f8195cc576b9ee81f93135536141dbbd438e37047be08e1f15048fa18d7a32f1887a883
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,75 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+ SuggestExtensions: false
7
+
8
+ Gemspec/RequireMFA:
9
+ Enabled: false
10
+
11
+ Layout/ExtraSpacing:
12
+ AllowForAlignment: true
13
+ Layout/LineLength:
14
+ Max: 80
15
+ Layout/MultilineMethodCallIndentation:
16
+ Enabled: false
17
+
18
+ Lint/AmbiguousBlockAssociation:
19
+ AllowedMethods:
20
+ - change
21
+ Lint/ConstantDefinitionInBlock:
22
+ Exclude:
23
+ - spec/spec_helper.rb
24
+ Lint/MissingSuper:
25
+ Enabled: false
26
+
27
+ Naming/MethodParameterName:
28
+ AllowedNames:
29
+ - by
30
+ - to
31
+ - id
32
+
33
+ Style/ArgumentsForwarding:
34
+ Enabled: false
35
+ Style/CharacterLiteral:
36
+ Enabled: false
37
+ Style/Documentation:
38
+ Enabled: false
39
+ Style/ExplicitBlockArgument:
40
+ Enabled: false
41
+ Style/FormatStringToken:
42
+ EnforcedStyle: template
43
+ Style/FrozenStringLiteralComment:
44
+ Enabled: false
45
+ Style/RescueStandardError:
46
+ EnforcedStyle: implicit
47
+ Style/StringLiterals:
48
+ EnforcedStyle: double_quotes
49
+
50
+ RSpec/AnyInstance:
51
+ Enabled: false
52
+ RSpec/ContextWording:
53
+ Prefixes:
54
+ - when
55
+ - with
56
+ - without
57
+ - if
58
+ - unless
59
+ - and
60
+ - but
61
+ RSpec/DescribedClass:
62
+ SkipBlocks: true
63
+ RSpec/ExpectChange:
64
+ EnforcedStyle: block
65
+ RSpec/FilePath:
66
+ Exclude:
67
+ - spec/kangaru/components/**/*.rb
68
+ RSpec/LetSetup:
69
+ Enabled: false
70
+ RSpec/MultipleMemoizedHelpers:
71
+ Max: 15
72
+ RSpec/NestedGroups:
73
+ Enabled: false
74
+ RSpec/SharedExamples:
75
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-09-05
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "fileutils"
6
+ gem "forwardable"
7
+ gem "pathname"
8
+ gem "rake", "~> 13.0"
9
+ gem "rspec", "~> 3.0"
10
+ gem "rubocop", "~> 1.21"
11
+ gem "rubocop-rspec", "~> 2.23"
12
+ gem "steep", "~> 1.5"
13
+ gem "yaml"
14
+ gem "zirconia", "~> 0.1"
data/Gemfile.lock ADDED
@@ -0,0 +1,139 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ kangaru (0.1.0)
5
+ colorize (~> 1.1)
6
+ erb (~> 4.0)
7
+ sequel (~> 5.72)
8
+ sqlite3 (~> 1.6)
9
+ zeitwerk (~> 2.6)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ activesupport (7.0.7.2)
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ i18n (>= 1.6, < 2)
17
+ minitest (>= 5.1)
18
+ tzinfo (~> 2.0)
19
+ ast (2.4.2)
20
+ base64 (0.1.1)
21
+ bigdecimal (3.1.4)
22
+ cgi (0.3.6)
23
+ colorize (1.1.0)
24
+ concurrent-ruby (1.2.2)
25
+ csv (3.2.7)
26
+ diff-lcs (1.5.0)
27
+ erb (4.0.3)
28
+ cgi (>= 0.3.3)
29
+ ffi (1.15.5)
30
+ fileutils (1.7.1)
31
+ forwardable (1.3.3)
32
+ i18n (1.14.1)
33
+ concurrent-ruby (~> 1.0)
34
+ json (2.6.3)
35
+ language_server-protocol (3.17.0.3)
36
+ listen (3.8.0)
37
+ rb-fsevent (~> 0.10, >= 0.10.3)
38
+ rb-inotify (~> 0.9, >= 0.9.10)
39
+ logger (1.5.3)
40
+ minitest (5.19.0)
41
+ parallel (1.23.0)
42
+ parser (3.2.2.3)
43
+ ast (~> 2.4.1)
44
+ racc
45
+ pathname (0.2.1)
46
+ racc (1.7.1)
47
+ rainbow (3.1.1)
48
+ rake (13.0.6)
49
+ rb-fsevent (0.11.2)
50
+ rb-inotify (0.10.1)
51
+ ffi (~> 1.0)
52
+ rbs (3.2.1)
53
+ regexp_parser (2.8.1)
54
+ rexml (3.2.6)
55
+ rspec (3.12.0)
56
+ rspec-core (~> 3.12.0)
57
+ rspec-expectations (~> 3.12.0)
58
+ rspec-mocks (~> 3.12.0)
59
+ rspec-core (3.12.2)
60
+ rspec-support (~> 3.12.0)
61
+ rspec-expectations (3.12.3)
62
+ diff-lcs (>= 1.2.0, < 2.0)
63
+ rspec-support (~> 3.12.0)
64
+ rspec-mocks (3.12.6)
65
+ diff-lcs (>= 1.2.0, < 2.0)
66
+ rspec-support (~> 3.12.0)
67
+ rspec-support (3.12.1)
68
+ rubocop (1.56.2)
69
+ base64 (~> 0.1.1)
70
+ json (~> 2.3)
71
+ language_server-protocol (>= 3.17.0)
72
+ parallel (~> 1.10)
73
+ parser (>= 3.2.2.3)
74
+ rainbow (>= 2.2.2, < 4.0)
75
+ regexp_parser (>= 1.8, < 3.0)
76
+ rexml (>= 3.2.5, < 4.0)
77
+ rubocop-ast (>= 1.28.1, < 2.0)
78
+ ruby-progressbar (~> 1.7)
79
+ unicode-display_width (>= 2.4.0, < 3.0)
80
+ rubocop-ast (1.29.0)
81
+ parser (>= 3.2.1.0)
82
+ rubocop-capybara (2.18.0)
83
+ rubocop (~> 1.41)
84
+ rubocop-factory_bot (2.23.1)
85
+ rubocop (~> 1.33)
86
+ rubocop-rspec (2.23.2)
87
+ rubocop (~> 1.33)
88
+ rubocop-capybara (~> 2.17)
89
+ rubocop-factory_bot (~> 2.22)
90
+ ruby-progressbar (1.13.0)
91
+ securerandom (0.2.2)
92
+ sequel (5.72.0)
93
+ bigdecimal
94
+ sqlite3 (1.6.0-arm64-darwin)
95
+ sqlite3 (1.6.0-x86_64-linux)
96
+ steep (1.5.3)
97
+ activesupport (>= 5.1)
98
+ concurrent-ruby (>= 1.1.10)
99
+ csv (>= 3.0.9)
100
+ fileutils (>= 1.1.0)
101
+ json (>= 2.1.0)
102
+ language_server-protocol (>= 3.15, < 4.0)
103
+ listen (~> 3.0)
104
+ logger (>= 1.3.0)
105
+ parser (>= 3.1)
106
+ rainbow (>= 2.2.2, < 4.0)
107
+ rbs (>= 3.1.0)
108
+ securerandom (>= 0.1)
109
+ strscan (>= 1.0.0)
110
+ terminal-table (>= 2, < 4)
111
+ strscan (3.0.6)
112
+ terminal-table (3.0.2)
113
+ unicode-display_width (>= 1.1.1, < 3)
114
+ tzinfo (2.0.6)
115
+ concurrent-ruby (~> 1.0)
116
+ unicode-display_width (2.4.2)
117
+ yaml (0.2.1)
118
+ zeitwerk (2.6.11)
119
+ zirconia (0.1.2)
120
+
121
+ PLATFORMS
122
+ arm64-darwin-21
123
+ x86_64-linux
124
+
125
+ DEPENDENCIES
126
+ fileutils
127
+ forwardable
128
+ kangaru!
129
+ pathname
130
+ rake (~> 13.0)
131
+ rspec (~> 3.0)
132
+ rubocop (~> 1.21)
133
+ rubocop-rspec (~> 2.23)
134
+ steep (~> 1.5)
135
+ yaml
136
+ zirconia (~> 0.1)
137
+
138
+ BUNDLED WITH
139
+ 2.4.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Chris Welham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Kangaru - A Ruby Framework for Building Command Line Applications
2
+
3
+ Kangaru is an open-source framework written in Ruby for building powerful and efficient command line applications. This project aims to make it easier for developers to create complex CLI programs with minimal effort, by providing a set of useful tools and libraries in a configurable ecosystem.
4
+
5
+ ## Features
6
+
7
+ - MVC architecture drawing heavy inspiration from Rails
8
+ - ERB rendering of view templates with embedded Ruby capabilities
9
+ - SQLite and Sequel integration allowing databases to be created and used by your CLI
10
+ - A powerful command parser that routes terminal requests to their controller
11
+ - Zeitwerk-based autoloading of gem files
12
+ - Simple installation, integration and configuration
13
+
14
+
15
+ ## Quick Setup
16
+
17
+ It is recommended to create a new Ruby gem for each Kangaru CLI application. This can be executed through `bundler` with the following command:
18
+
19
+ ```sh
20
+ bundle gem gem_name
21
+ ```
22
+
23
+ Next add Kangaru to your Gem's Gemfile and bundle install:
24
+
25
+ ```ruby
26
+ # Gemfile
27
+ gem "kangaru"
28
+ ```
29
+ ```sh
30
+ bundle install
31
+ ```
32
+
33
+ Kangaru's set up is bundled up into a single Ruby module that must be extended from the entry file for your Gem application. Conventionally this will be the Ruby file at `/gem_name/lib/gem_name.rb`, where `gem_name` is the name of the application specified with the `bundle gem` command.
34
+
35
+ For example:
36
+
37
+ ```ruby
38
+ # gem_name/lib/gem_name.rb
39
+ require "kangaru"
40
+
41
+ module GemName
42
+ extend Kangaru::Initialiser
43
+ end
44
+ ```
45
+
46
+ This extension will set up Kangaru in the context of your Gem, including setting up processes such as the autoloader and config.
47
+
48
+ ## Documentation
49
+ - TBC
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require "rubocop/rake_task"
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
data/Steepfile ADDED
@@ -0,0 +1,12 @@
1
+ target :lib do
2
+ signature "sig"
3
+
4
+ check "lib"
5
+
6
+ configure_code_diagnostics do |hash|
7
+ hash[Steep::Diagnostic::Ruby::FallbackAny] = nil
8
+ hash[Steep::Diagnostic::Ruby::UnknownConstant] = :error
9
+ hash[Steep::Diagnostic::Ruby::MethodDefinitionMissing] = nil
10
+ hash[Steep::Diagnostic::Ruby::UnsupportedSyntax] = :hint
11
+ end
12
+ end
data/kangaru.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ require_relative "lib/kangaru/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "kangaru"
5
+ spec.version = Kangaru::VERSION
6
+ spec.authors = ["Chris Welham"]
7
+ spec.email = ["71787007+apexatoll@users.noreply.github.com"]
8
+
9
+ spec.summary = "A lightweight framework for building command line interfaces"
10
+ spec.license = "MIT"
11
+ spec.required_ruby_version = ">= 3.2.0"
12
+
13
+ spec.files = Dir.chdir(__dir__) do
14
+ `git ls-files`.split("\n").reject do |f|
15
+ f == __FILE__ || f.match?(/\A(bin|sig|spec|\.git|\.github)/)
16
+ end
17
+ end
18
+ spec.bindir = "bin"
19
+ spec.executables = []
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "colorize", "~> 1.1"
23
+ spec.add_dependency "erb", "~> 4.0"
24
+ spec.add_dependency "sequel", "~> 5.72"
25
+ spec.add_dependency "sqlite3", "~> 1.6"
26
+ spec.add_dependency "zeitwerk", "~> 2.6"
27
+ end
@@ -0,0 +1,73 @@
1
+ module Kangaru
2
+ class Application
3
+ extend Forwardable
4
+
5
+ attr_reader :paths, :namespace, :database
6
+
7
+ def initialize(source:, namespace:)
8
+ @paths = Paths.new(source:)
9
+ @namespace = namespace
10
+
11
+ autoloader.setup
12
+ end
13
+
14
+ # Lazy-loaded to allow defaults to be set after application is created.
15
+ def config
16
+ @config ||= Config.new
17
+ end
18
+
19
+ # If called with no env, the config will be applied regardless of current
20
+ # env. If multiple configure calls matching the current env are made, the
21
+ # most recent calls will overwrite older changes.
22
+ def configure(env = nil, &block)
23
+ block.call(config) if current_env?(env)
24
+ end
25
+
26
+ def configured?
27
+ @configured == true
28
+ end
29
+
30
+ def apply_config!
31
+ raise "config already applied" if configured?
32
+
33
+ config.import_external_config!
34
+
35
+ @database = setup_database!
36
+ @configured = true
37
+ end
38
+
39
+ def run!(*argv)
40
+ request = RequestBuilder.new(argv).build
41
+
42
+ Router.new(request, namespace:).resolve
43
+ end
44
+
45
+ def_delegators :paths, :view_path
46
+
47
+ private
48
+
49
+ # Returns true if nil as this is represents all envs.
50
+ def current_env?(env)
51
+ return true if env.nil?
52
+
53
+ Kangaru.env?(env)
54
+ end
55
+
56
+ def autoloader
57
+ @autoloader ||= Zeitwerk::Loader.new.tap do |loader|
58
+ loader.inflector = Zeitwerk::GemInflector.new(paths.source.to_s)
59
+ loader.collapse(paths.collapsed_dirs)
60
+ loader.push_dir(paths.lib_path.to_s)
61
+ end
62
+ end
63
+
64
+ def setup_database!
65
+ return unless config.database.adaptor
66
+
67
+ Database.new(**config.database.serialise).tap do |database|
68
+ database.setup!
69
+ database.migrate!
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,34 @@
1
+ module Kangaru
2
+ class ArgumentParser
3
+ using Patches::Inflections
4
+
5
+ attr_reader :tokens
6
+
7
+ def initialize(*tokens)
8
+ @tokens = tokens
9
+ end
10
+
11
+ def parse
12
+ grouped_argument_tokens.to_h do |tokens|
13
+ key = parse_key(tokens.shift || raise)
14
+ value = parse_value(tokens)
15
+
16
+ [key, value]
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def grouped_argument_tokens
23
+ tokens.slice_before(/^-/).to_a
24
+ end
25
+
26
+ def parse_key(key)
27
+ key.gsub(/^-+/, "").to_snakecase.to_sym
28
+ end
29
+
30
+ def parse_value(tokens)
31
+ tokens.empty? || tokens.join(" ")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ module Kangaru
2
+ class Component
3
+ using Patches::Inflections
4
+ using Patches::Source
5
+
6
+ def render
7
+ Renderer.new(view_file).render(binding)
8
+ end
9
+
10
+ private
11
+
12
+ def view_file
13
+ dir = File.dirname(self.class.source)
14
+ name = File.basename(self.class.source, ".rb")
15
+
16
+ Pathname.new(dir).join("#{name}.erb")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ module Kangaru
2
+ module Concerns
3
+ module AttributesConcern
4
+ extend Concern
5
+
6
+ class_methods do
7
+ def attributes
8
+ instance_methods.grep(/\w=$/).map do |attribute|
9
+ attribute.to_s.delete_suffix("=").to_sym
10
+ end
11
+ end
12
+
13
+ def set_default(**attributes)
14
+ defaults.merge!(**attributes)
15
+ end
16
+
17
+ def defaults
18
+ @defaults ||= {}
19
+ end
20
+ end
21
+
22
+ def initialize(**attributes)
23
+ attributes = self.class.defaults.merge(**attributes)
24
+
25
+ attributes.slice(*self.class.attributes).each do |attr, value|
26
+ instance_variable_set(:"@#{attr}", value)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ module Kangaru
2
+ module Concerns
3
+ module Concern
4
+ def append_features(base)
5
+ super
6
+ evaluate_concern_blocks!(base)
7
+ end
8
+
9
+ def class_methods(&)
10
+ if const_defined?(:ClassMethods)
11
+ const_get(:ClassMethods)
12
+ else
13
+ const_set(:ClassMethods, Module.new)
14
+ end.module_eval(&)
15
+ end
16
+
17
+ def included(base = nil, &block)
18
+ super base if base
19
+ return if block.nil?
20
+
21
+ @included = block
22
+ end
23
+
24
+ private
25
+
26
+ def evaluate_concern_blocks!(base)
27
+ base.extend(const_get(:ClassMethods)) if const_defined?(:ClassMethods)
28
+
29
+ base.class_eval(&@included) if instance_variable_defined?(:@included)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Kangaru
2
+ module Concerns
3
+ module Configurable
4
+ extend Concern
5
+
6
+ using Patches::Constantise
7
+
8
+ class_methods do
9
+ def configurator_name
10
+ (name || raise("class name not set"))
11
+ .gsub(/^(.*?)::/, "\\1::Configurators::")
12
+ .concat("Configurator")
13
+ end
14
+ end
15
+
16
+ def config
17
+ Kangaru.application.config.for(self.class.configurator_name) ||
18
+ raise("inferred configurator not set by application")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ module Kangaru
2
+ module Concerns
3
+ module Validatable
4
+ extend Concern
5
+
6
+ class_methods do
7
+ def validation_rules
8
+ @validation_rules ||= {}
9
+ end
10
+
11
+ def validates(attribute, **validations)
12
+ validation_rules[attribute] ||= {}
13
+ validation_rules[attribute].merge!(**validations)
14
+ end
15
+ end
16
+
17
+ def errors
18
+ @errors ||= []
19
+ end
20
+
21
+ def validate
22
+ self.class.validation_rules.each do |attribute, validations|
23
+ validator = validator_for(attribute)
24
+
25
+ validations.each do |validator_name, params|
26
+ params = {} if params == true
27
+
28
+ validator.validate(validator_name, **params)
29
+ end
30
+ end
31
+ end
32
+
33
+ def valid?
34
+ validate
35
+
36
+ errors.empty?
37
+ end
38
+
39
+ private
40
+
41
+ def validator_for(attribute)
42
+ Validation::AttributeValidator.new(model: self, attribute:)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ module Kangaru
2
+ class Config
3
+ attr_reader :configurators
4
+
5
+ def initialize
6
+ @configurators = set_configurators!
7
+ end
8
+
9
+ def serialise
10
+ configurators.transform_values(&:serialise)
11
+ end
12
+
13
+ def import_external_config!
14
+ return unless external_config_exists?
15
+
16
+ @external = Configurators::ExternalConfigurator
17
+ .from_yaml_file(application.config_path)
18
+ end
19
+
20
+ # Returns the configurator instance with the given class name.
21
+ def for(configurator_name)
22
+ configurators.values.find do |configurator|
23
+ configurator.class.name == configurator_name # rubocop:disable Style
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def set_configurators!
30
+ Configurators.classes.to_h do |configurator_class|
31
+ self.class.class_eval { attr_reader configurator_class.key }
32
+
33
+ configurator = configurator_class.new
34
+
35
+ instance_variable_set(:"@#{configurator_class.key}", configurator)
36
+
37
+ [configurator_class.key, configurator]
38
+ end
39
+ end
40
+
41
+ def external_config_exists?
42
+ application.config_path && File.exist?(application.config_path)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ module Kangaru
2
+ module Configurators
3
+ class ApplicationConfigurator < Configurator
4
+ attr_accessor :config_path
5
+ end
6
+ end
7
+ end