rackal 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0ead58575158b4ee56a407d2dc1efc3b9ca7dbe00fef20cfd2f6151fbc1a6585
4
+ data.tar.gz: 2b325e4277a93d86795c280acd6e3d83b4ec8090322e2f198889379d5e6e9244
5
+ SHA512:
6
+ metadata.gz: b407bd38f1d6e5dac470a165c8111b022150b501acd0f1b59b40c9a71216a7650888295aed80a8969e2206fec9e76b6f33fbf439c76e626eda430cd8caffcdba
7
+ data.tar.gz: 53a49cea9ac29e8bd342867ae1714f72acdf5a3cf457b6d7d63a9962bbad9a5944bb186e9f9b627ae2c34e7c9950f3c7871aca2511d3df724f81bb34468defd4
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Copyright (c) 2021 Richard Newman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ software and associated documentation files (the "Software"), to deal in the Software
5
+ without restriction, including without limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
+ persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
15
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
16
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
17
+ OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Rackal
2
+
3
+ The `rackal` gem provides helpers in configuring directly configuring Rack.
4
+
5
+ The main intent is abstract away some common boilerplate and reduce hardcoded details in
6
+ application configuration, especially in `config.ru`, in a way that still allows some
7
+ direct control of how it is applied.
8
+
9
+ Rackal was originally developed to simplify set up of Roda apps but is intended to not
10
+ assume or depend on Roda. As of this writing, no testing with Sinatra or bare Rack apps has
11
+ been performed, so milage may vary. This gem is still considered experimental.
12
+
13
+ ## Features
14
+ * Environment aware: "production", "staging", "test", and "development" are recognized (based on ENV['RACK_ENV'])
15
+ * Database configuration: easy access to database configuration (based on YAML)
16
+ * Application metadata: easy access to application details (trivial YAML configuration)
17
+ * Application protection: easy use of Refrigerator gem if used
18
+
19
+ ## Example `config.ru`
20
+ ```
21
+ require 'rackal'
22
+
23
+ # support logging
24
+ if Rackal.environment.unprotected? # by default: "development" or "test" is unprotected
25
+ require 'logger'
26
+ logger = Logger.new($stdout)
27
+ end
28
+
29
+ require 'roda'
30
+
31
+ Gem.ruby
32
+
33
+ # these output lines demonstrate some use
34
+ puts "Starting #{Rackal.application.name} in #{Rackal.environment.env} environment."
35
+ #=> Starting MyApp in production environment.
36
+ puts "(root directory is #{Rackal.application.root})"
37
+ #=> (root directory is /home/someuser/src/my_app)
38
+
39
+ run(Rackal.application.main_class.freeze.app) # main_class is the driving class of the application
40
+
41
+ Rackal.protect! # applies Refrigerator if gem is present in production settings
42
+ ```
43
+
44
+ ## Performance
45
+ Given that one key feature of Roda is its performance and memory footprint, care was taken
46
+ to avoid interferring with that. Any cost is constrained as much as possible to only
47
+ application startup:
48
+ * Rackal is expected to add minimal time to application startup.
49
+ * Rackal is expected to consume marginally additional memory in the application for holding its
50
+ state once the application has started.
51
+
52
+ Rigorous performance testing has not yet been done to ensure those claims.
53
+
54
+ ## Contributing
55
+ Since a Gemfile is used for testing and development conveniences without polluting the
56
+ gem itself, be sure to `bundle install`. RSpec, simplecov, and rubocop are assumed, but
57
+ only RSpec itself is required.
58
+
59
+ The provided RSpec tests have strong coverage. Presently focus on mutation testing is
60
+ ongoing.
61
+
62
+ Further development is invited to better support Roda applications, extensions and
63
+ generalization for non-Roda Rack applications, and performance improvements.
64
+
65
+ ## Acknowledgements
66
+ Jeremy Evans's `roda-sequel-stack` [repository](https://github.com/jeremyevans/roda-sequel-stack) was used as the original reference for boilerplate.
67
+
68
+ Some application metadata concepts were inspired by Rails, with a much lighter approach taken.
69
+
70
+ ## License
71
+ Rackal is released under the [MIT License](https://opensource.org/licenses/MIT).
data/lib/rackal.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'rackal/internal/application'
2
+ require 'rackal/internal/database_configuration'
3
+ require 'rackal/internal/rack_environment'
4
+ require 'rackal/internal/protection'
5
+
6
+ module Rackal
7
+ # Retrieves current Rack environment with convenience methods
8
+ #
9
+ # @param [Hash] options
10
+ # @option options [true, false] :reset If true, resets cache and ensure fresh retrieval (defaults to: false)
11
+ # @return [Object] an object
12
+ def self.environment(options = {})
13
+ reload = options.delete(:reload) || false
14
+ @environment = nil if reload
15
+
16
+ @environment = Internal::RackEnvironment.new(options)
17
+ end
18
+
19
+ def self.database_parameters(options = {})
20
+ reload = options.delete(:reload) || false
21
+ @database_parameters = nil if reload
22
+
23
+ @database_parameters ||= Internal::DatabaseConfiguration.parameters
24
+ end
25
+
26
+ def self.env(key, options = {})
27
+ reload = options.delete(:reload) || false
28
+ @env = nil if reload
29
+
30
+ @env ||= {}
31
+
32
+ @env[key] || (@env[key] = ENV.fetch(key.to_s, nil))
33
+ end
34
+
35
+ def self.application
36
+ @application ||= Internal::Application.new
37
+ end
38
+
39
+ def self.protect!
40
+ Internal::Protection.apply
41
+ end
42
+
43
+ def self.root
44
+ @root ||= application.root
45
+ end
46
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'configuration_file'
2
+
3
+ module Rackal
4
+ module Internal
5
+ class Application
6
+ include ConfigurationFile
7
+
8
+ # TODO: -- drop this?!
9
+ def self.root
10
+ new.root
11
+ end
12
+
13
+ def initialize
14
+ @config = parse
15
+ end
16
+
17
+ def name
18
+ @config[:app_main]
19
+ end
20
+
21
+ # only supports bare class names (not under a namespace)
22
+ def main_class
23
+ raise NameError unless name
24
+
25
+ @main_class ||= Object.const_get(name)
26
+ end
27
+
28
+ # lightly inspired by Rails implementation in
29
+ # railties/lib/rails/engine.rb#find_root_with_flag
30
+ def root
31
+ return @root if defined?(@root) && @root
32
+
33
+ paths = potential_root_paths
34
+
35
+ # 1) preference is to find "config.ru" if possible
36
+ # 2) if not (1), try to find main app
37
+ # 3) if not (1) or (2) look for nearest lib/ directory
38
+ [:configru, :app, :lib].each do |key|
39
+ info = paths.detect { |path| path[key] }
40
+ @root = info&.fetch(:dirname, nil)
41
+ return @root if @root
42
+ end
43
+
44
+ # give up if we can't find it
45
+ @root = Dir.pwd
46
+ end
47
+
48
+ private
49
+
50
+ def parse
51
+ @parse = read_configuration('rackal') { |content| content.fetch('rackal') }
52
+ end
53
+
54
+ # only works in linux style OS (assumes path delimiter "/")
55
+ def potential_root_paths
56
+ appname = main_class_rb_filename
57
+
58
+ caller_locations.lazy.map { |location| assess_location(location, appname) }
59
+ # assess_location
60
+ # path = location.absolute_path || location.path
61
+ # dirname = File.dirname(path)
62
+
63
+ # {
64
+ # dirname: dirname,
65
+ # configru: File.directory?(dirname) && File.exist?("#{dirname}/config.ru"),
66
+ # app: appname && (File.directory?(dirname) && File.exist?("#{dirname}/#{appname}")),
67
+ # # configru: File.directory?(path) && File.exist?("#{path}/config.ru"),
68
+ # # app: appname && (File.directory?(path) && File.exist?("#{path}/#{appname}")),
69
+ # lib: dirname.match?(/.*\/lib\/\z/)
70
+ # }
71
+ # end
72
+ end
73
+
74
+ def assess_location(location, appname)
75
+ path = location.absolute_path || location.path
76
+ dirname = File.dirname(path)
77
+
78
+ {
79
+ dirname: dirname,
80
+ configru: File.directory?(dirname) && File.exist?("#{dirname}/config.ru"),
81
+ app: appname && (File.directory?(dirname) && File.exist?("#{dirname}/#{appname}")),
82
+ # configru: File.directory?(path) && File.exist?("#{path}/config.ru"),
83
+ # app: appname && (File.directory?(path) && File.exist?("#{path}/#{appname}")),
84
+ lib: dirname.match?(/.*\/lib\/\z/)
85
+ }
86
+ end
87
+
88
+ def main_class_rb_filename
89
+ return nil unless name
90
+
91
+ # stolen from Rails ActiveSupport (but simpler)
92
+ word = name.gsub(/([A-Z\d]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do
93
+ (Regexp.last_match(1) || Regexp.last_match(2)) << '_'
94
+ end
95
+ "#{word.tr('-', '_').downcase}.rb"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'yaml_file'
2
+
3
+ module Rackal
4
+ module Internal
5
+ module ConfigurationFile
6
+ include YamlFile
7
+
8
+ def configuration_directory
9
+ 'config'
10
+ end
11
+
12
+ def read_configuration(yaml_filename)
13
+ filename = yaml_filename&.strip || ''
14
+ if filename.empty?
15
+ raise ArgumentError, "must apply a YAML filename within #{configuration_directory}"
16
+ end
17
+
18
+ filepath = "#{configuration_directory}/#{yaml_filename}.yml"
19
+ content = yaml_content(filepath)
20
+ (block_given? ? yield(content) : content)&.transform_keys(&:to_sym)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'configuration_file'
2
+
3
+ module Rackal
4
+ module Internal
5
+ class DatabaseConfiguration
6
+ include ConfigurationFile
7
+
8
+ attr_reader :parameters
9
+
10
+ class << self
11
+ def parameters
12
+ new.parameters
13
+ end
14
+ end
15
+
16
+ def initialize
17
+ @parameters = parse
18
+ end
19
+
20
+ private
21
+
22
+ def parse
23
+ requested_key = Rackal.environment.env.to_s
24
+ used_key = requested_key
25
+
26
+ result = read_configuration('database') do |content|
27
+ content.fetch(requested_key) { |_| content.fetch(used_key = 'default') }
28
+ end
29
+
30
+ result[:configuration_key] = {
31
+ requested: requested_key,
32
+ used: used_key
33
+ }
34
+
35
+ result
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ module Rackal
2
+ module Internal
3
+ module Protection
4
+ def self.apply
5
+ return false if Rackal.environment.unprotected?
6
+
7
+ begin
8
+ require 'refrigerator'
9
+ rescue LoadError
10
+ false
11
+ else
12
+ Refrigerator.freeze_core
13
+ true
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ module Rackal
2
+ module Internal
3
+ class RackEnvironment
4
+ attr_reader :env
5
+
6
+ # @api private
7
+ def initialize(options = {})
8
+ @env = (ENV['RACK_ENV'] || 'development').to_sym
9
+
10
+ protect_test = options&.fetch(:protect_test, false) || false
11
+ @protect_test = protect_test ? true : false # force boolean
12
+ end
13
+
14
+ # @return Array list of environments recognized as supported by RackEnvironment
15
+ def self.supported
16
+ [:production, :staging, :test, :development]
17
+ end
18
+
19
+ supported.each do |supported_name|
20
+ # @!macro [attach] supported_name
21
+ # @method $1?()
22
+ # @return Boolean true if current environment is $1
23
+ define_method("#{supported_name}?") { env == supported_name }
24
+ end
25
+
26
+ # @return Boolean true if current environment is supported
27
+ def supported?
28
+ @supported ||= self.class.supported.include? env
29
+ end
30
+
31
+ # @return Boolean true if current environment is not supported
32
+ def unsupported?
33
+ !supported?
34
+ end
35
+
36
+ # return Array list environments treated as protected (e.g., :production)
37
+ def protected
38
+ @protected ||= self.class.supported -
39
+ (@protect_test ? [:development] : [:development, :test])
40
+ end
41
+
42
+ # @return Boolean true if current environment is to be treated as protected
43
+ def protected?
44
+ @is_protected ||= protected.include? env
45
+ end
46
+
47
+ # @return Boolean true if current environment is not to be treated as protected
48
+ def unprotected?
49
+ @is_unprotected ||= !protected?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ module Rackal
5
+ module Internal
6
+ module YamlFile
7
+ def yaml_content(filepath)
8
+ raise ArgumentError unless filepath.is_a?(String)
9
+
10
+ YAML.safe_load(
11
+ ERB.new(File.read(filepath)).result,
12
+ aliases: true
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rackal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Richard Newman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-07-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Rack application helpers
14
+
15
+ '
16
+ email:
17
+ - richard@newmanworks.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - lib/rackal.rb
25
+ - lib/rackal/internal/application.rb
26
+ - lib/rackal/internal/configuration_file.rb
27
+ - lib/rackal/internal/database_configuration.rb
28
+ - lib/rackal/internal/protection.rb
29
+ - lib/rackal/internal/rack_environment.rb
30
+ - lib/rackal/internal/yaml_file.rb
31
+ homepage: https://rubygems.org/gems/rackal
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.6'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.1.4
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Rackal
54
+ test_files: []