global 0.2.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,11 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
 
4
6
  Bundler.require
5
7
 
6
8
  require 'rspec/core/rake_task'
7
- require "bundler/gem_tasks"
9
+ require 'bundler/gem_tasks'
10
+ require 'rubocop/rake_task'
8
11
 
9
12
  RSpec::Core::RakeTask.new(:spec)
13
+ RuboCop::RakeTask.new(:rubocop) do |task|
14
+ task.options = ['-D'] # Display cop name
15
+ task.fail_on_error = true
16
+ end
10
17
 
11
- task :default => :spec
18
+ desc 'Run all tests'
19
+ task default: %i[rubocop spec]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'global'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/global.gemspec CHANGED
@@ -1,33 +1,31 @@
1
- $:.push File.expand_path("../lib", __FILE__)
2
- require "global/version"
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+ require 'global/version'
3
5
 
4
6
  Gem::Specification.new do |s|
5
- s.name = "global"
7
+ s.name = 'global'
6
8
  s.version = Global::VERSION
7
- s.authors = ["Railsware LLC"]
8
- s.email = "contact@railsware.com"
9
-
10
- s.rubyforge_project = "global"
9
+ s.authors = ['Railsware LLC']
10
+ s.email = 'contact@railsware.com'
11
11
 
12
- s.description = "Simple way to load your configs from yaml"
13
- s.summary = "Simple way to load your configs from yaml"
12
+ s.description = 'Simple way to load your configs from yaml/aws/gcp'
13
+ s.summary = 'Simple way to load your configs from yaml/aws/gcp'
14
14
 
15
15
  s.files = `git ls-files`.split("\n")
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
- s.require_paths = ["lib"]
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
18
+ s.require_paths = ['lib']
19
19
 
20
- s.homepage = "https://github.com/railsware/global"
21
- s.licenses = ["MIT"]
20
+ s.homepage = 'https://github.com/railsware/global'
21
+ s.licenses = ['MIT']
22
22
 
23
- s.add_development_dependency "rspec", ">= 3.0"
24
- s.add_development_dependency "simplecov", "~> 0.7.1"
25
- s.add_development_dependency "rake", "~> 10.1.0"
26
- if defined?(JRUBY_VERSION)
27
- s.add_development_dependency "therubyrhino", ">= 0"
28
- else
29
- s.add_development_dependency "therubyracer", ">= 0"
30
- end
23
+ s.add_development_dependency 'aws-sdk-ssm', '~> 1'
24
+ s.add_development_dependency 'google-cloud-secret_manager', '~> 0'
25
+ s.add_development_dependency 'rake', '~> 12.3.1'
26
+ s.add_development_dependency 'rspec', '>= 3.0'
27
+ s.add_development_dependency 'rubocop', '~> 0.81.0'
28
+ s.add_development_dependency 'simplecov', '~> 0.16.1'
31
29
 
32
- s.add_runtime_dependency "activesupport", ">= 2.0"
30
+ s.add_runtime_dependency 'activesupport', '>= 2.0'
33
31
  end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Global
4
+ module Backend
5
+ # Loads Global configuration from the AWS Systems Manager Parameter Store
6
+ # https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html
7
+ #
8
+ # This backend requires the `aws-sdk` or `aws-sdk-ssm` gem, so make sure to add it to your Gemfile.
9
+ #
10
+ # Available options:
11
+ # - `prefix` (required): the prefix in Parameter Store; all parameters within the prefix will be loaded;
12
+ # make sure to add a trailing slash, if you want it
13
+ # see https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-su-organize.html
14
+ # - `client`: pass you own Aws::SSM::Client instance, or alternatively set:
15
+ # - `aws_options`: credentials and other AWS configuration options that are passed to AWS::SSM::Client.new
16
+ # see https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SSM/Client.html#initialize-instance_method
17
+ # If AWS access is already configured through environment variables,
18
+ # you don't need to pass the credentials explicitly.
19
+ #
20
+ # For Rails:
21
+ # - the `prefix` is optional and defaults to `/[Rails enviroment]/[Name of the app class]/`,
22
+ # for example: `/production/MyApp/`
23
+ # - to use a different app name, pass `app_name`,
24
+ # for example: `backend :aws_parameter_store, app_name: 'new_name_for_my_app'`
25
+ class AwsParameterStore
26
+
27
+ PATH_SEPARATOR = '/'
28
+
29
+ def initialize(options = {})
30
+ require_aws_gem
31
+ init_prefix(options)
32
+ init_client(options)
33
+ end
34
+
35
+ def load
36
+ build_configuration_from_parameters(load_all_parameters_from_ssm)
37
+ end
38
+
39
+ private
40
+
41
+ def require_aws_gem
42
+ require 'aws-sdk-ssm'
43
+ rescue LoadError
44
+ begin
45
+ require 'aws-sdk'
46
+ rescue LoadError
47
+ raise 'Either the `aws-sdk-ssm` or `aws-sdk` gem must be installed.'
48
+ end
49
+ end
50
+
51
+ def init_prefix(options)
52
+ @prefix = if defined?(Rails)
53
+ options.fetch(:prefix) do
54
+ environment = Rails.env.to_s
55
+ app_name = options.fetch(:app_name) { Rails.application.class.module_parent_name }
56
+ "/#{environment}/#{app_name}/"
57
+ end
58
+ else
59
+ options.fetch(:prefix)
60
+ end
61
+ end
62
+
63
+ def init_client(options)
64
+ if options.key?(:client)
65
+ @ssm = options[:client]
66
+ else
67
+ aws_options = options.fetch(:aws_options, {})
68
+ @ssm = Aws::SSM::Client.new(aws_options)
69
+ end
70
+ end
71
+
72
+ def load_all_parameters_from_ssm
73
+ response = load_parameters_from_ssm
74
+ all_parameters = response.parameters
75
+ loop do
76
+ break unless response.next_token
77
+
78
+ response = load_parameters_from_ssm(response.next_token)
79
+ all_parameters.concat(response.parameters)
80
+ end
81
+
82
+ all_parameters
83
+ end
84
+
85
+ def load_parameters_from_ssm(next_token = nil)
86
+ @ssm.get_parameters_by_path(
87
+ path: @prefix,
88
+ recursive: true,
89
+ with_decryption: true,
90
+ next_token: next_token
91
+ )
92
+ end
93
+
94
+ # builds a nested configuration hash from the array of parameters from SSM
95
+ def build_configuration_from_parameters(parameters)
96
+ configuration = {}
97
+ parameters.each do |parameter|
98
+ parameter_parts = parameter.name[@prefix.length..-1].split(PATH_SEPARATOR).map(&:to_sym)
99
+ param_container = parameter_parts[0..-2].reduce(configuration) do |container, part|
100
+ container[part] ||= {}
101
+ end
102
+ param_container[parameter_parts[-1]] = parameter.value
103
+ end
104
+
105
+ configuration
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Global
4
+ module Backend
5
+ # Loads Global configuration from the filesystem
6
+ #
7
+ # Available options:
8
+ # - `path` (required): the directory with config files
9
+ # - `environment` (required): the environment to load
10
+ # - `yaml_whitelist_classes`: the set of classes that are permitted to unmarshal from the configuration files
11
+ #
12
+ # For Rails:
13
+ # - the `path` is optional and defaults to `config/global`
14
+ # - the `environment` is optional and defaults to the current Rails environment
15
+ class Filesystem
16
+
17
+ FILE_ENV_SPLIT = '.'
18
+ YAML_EXT = '.yml'
19
+
20
+ def initialize(options = {})
21
+ if defined?(Rails)
22
+ @path = options.fetch(:path) { Rails.root.join('config', 'global').to_s }
23
+ @environment = options.fetch(:environment) { Rails.env.to_s }
24
+ else
25
+ @path = options.fetch(:path)
26
+ @environment = options.fetch(:environment)
27
+ end
28
+ @yaml_whitelist_classes = options.fetch(:yaml_whitelist_classes, [])
29
+ end
30
+
31
+ def load
32
+ load_from_path(@path)
33
+ end
34
+
35
+ private
36
+
37
+ def load_from_path(path)
38
+ load_from_file(path).deep_merge(load_from_directory(path))
39
+ end
40
+
41
+ def load_from_file(path)
42
+ config = {}
43
+
44
+ if File.exist?(file = "#{path}#{YAML_EXT}")
45
+ configurations = load_yml_file(file)
46
+ config = get_config_by_key(configurations, 'default')
47
+ config.deep_merge!(get_config_by_key(configurations, @environment))
48
+ if File.exist?(env_file = "#{path}#{FILE_ENV_SPLIT}#{@environment}#{YAML_EXT}")
49
+ config.deep_merge!(load_yml_file(env_file) || {})
50
+ end
51
+ end
52
+
53
+ config
54
+ end
55
+
56
+ def get_config_by_key(config, key)
57
+ return {} if config.empty?
58
+
59
+ config[key.to_sym] || config[key.to_s] || {}
60
+ end
61
+
62
+ def load_yml_file(file)
63
+ file_contents = ERB.new(IO.read(file)).result
64
+ permitted_classes = [Date, Time, DateTime, Symbol].concat(@yaml_whitelist_classes)
65
+
66
+ if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('4')
67
+ YAML.safe_load(file_contents, permitted_classes: permitted_classes, aliases: true)
68
+ else
69
+ YAML.safe_load(file_contents, permitted_classes, [], true)
70
+ end
71
+ end
72
+
73
+ def load_from_directory(path)
74
+ config = {}
75
+
76
+ if File.directory?(path)
77
+ Dir["#{path}/*"].each do |entry|
78
+ namespace = File.basename(entry, YAML_EXT)
79
+ next if namespace.include? FILE_ENV_SPLIT # skip files with dot(s) in name
80
+
81
+ file_with_path = File.join(File.dirname(entry), File.basename(entry, YAML_EXT))
82
+ config.deep_merge!(namespace => load_from_path(file_with_path))
83
+ end
84
+ end
85
+
86
+ config
87
+ end
88
+
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Global
4
+ module Backend
5
+ # Loads Global configuration from the Google Cloud Secret Manager
6
+ # https://cloud.google.com/secret-manager/docs
7
+ #
8
+ # This backend requires the `google-cloud-secret_manager` gem, so make sure to add it to your Gemfile.
9
+ #
10
+ # Available options:
11
+ # - `project_id` (required): Google Cloud project name
12
+ # - `prefix` (required): the prefix in Secret Manager; all parameters within the prefix will be loaded;
13
+ # make sure to add a undescore, if you want it
14
+ # see https://cloud.google.com/secret-manager/docs/overview
15
+ # - `client`: pass you own Google::Cloud::SecretManager instance, or alternatively set:
16
+ # - `gcp_options`: credentials and other Google cloud configuration options
17
+ # that are passed to Google::Cloud::SecretManager.configure
18
+ # see https://googleapis.dev/ruby/google-cloud-secret_manager/latest/index.html
19
+ # If Google Cloud access is already configured through environment variables,
20
+ # you don't need to pass the credentials explicitly.
21
+ #
22
+ # For Rails:
23
+ # - the `prefix` is optional and defaults to `[Rails enviroment]-[Name of the app class]-`,
24
+ # for example: `production-myapp-`
25
+ # - to use a different app name, pass `app_name`,
26
+ # for example: `backend :gcp_secret_manager, app_name: 'new_name_for_my_app'`
27
+ class GcpSecretManager
28
+
29
+ GCP_SEPARATOR = '/'
30
+ PATH_SEPARATOR = '-'
31
+
32
+ def initialize(options = {})
33
+ @project_id = options.fetch(:project_id)
34
+ require_gcp_gem
35
+ init_prefix(options)
36
+ init_client(options)
37
+ end
38
+
39
+ def load
40
+ pages = load_all_parameters_from_gcsm
41
+
42
+ configuration = {}
43
+ pages.each do |page|
44
+ configuration.deep_merge!(build_configuration_from_page(page))
45
+ end
46
+
47
+ configuration
48
+ end
49
+
50
+ private
51
+
52
+ def require_gcp_gem
53
+ require 'google/cloud/secret_manager'
54
+ rescue LoadError
55
+ raise 'The `google-cloud-secret_manager` gem must be installed.'
56
+ end
57
+
58
+ def init_prefix(options)
59
+ @prefix = if defined?(Rails)
60
+ options.fetch(:prefix) do
61
+ environment = Rails.env.to_s
62
+ app_name = options.fetch(:app_name) { Rails.application.class.module_parent_name }
63
+ "#{environment}-#{app_name}-"
64
+ end
65
+ else
66
+ options.fetch(:prefix)
67
+ end
68
+ end
69
+
70
+ def init_client(options)
71
+ if options.key?(:client)
72
+ @gcsm = options[:client]
73
+ else
74
+ gcp_options = options.fetch(:gcp_options, {})
75
+ @gcsm = Google::Cloud::SecretManager.secret_manager_service do |config|
76
+ config.credentials = gcp_options[:credentials] if gcp_options[:credentials]
77
+ config.timeout = gcp_options[:timeout] if gcp_options[:timeout]
78
+ end
79
+ end
80
+ end
81
+
82
+ def load_all_parameters_from_gcsm
83
+ response = load_parameters_from_gcsm
84
+ all_pages = [response]
85
+ loop do
86
+ break if response.next_page_token.empty?
87
+
88
+ response = load_parameters_from_gcsm(response.next_page_token)
89
+ all_pages << response
90
+ end
91
+
92
+ all_pages
93
+ end
94
+
95
+ def load_parameters_from_gcsm(next_token = nil)
96
+ @gcsm.list_secrets(
97
+ parent: @gcsm.project_path(project: @project_id),
98
+ page_size: 25_000,
99
+ page_token: next_token
100
+ )
101
+ end
102
+
103
+ # builds a nested configuration hash from the array of parameters from Secret Manager
104
+ def build_configuration_from_page(page)
105
+ configuration = {}
106
+
107
+ page.each do |parameter|
108
+ key_name = get_gcp_key_name(parameter)
109
+ next unless key_name.start_with?(@prefix)
110
+
111
+ parameter_parts = key_name[@prefix.length..-1].split(PATH_SEPARATOR).map(&:to_sym)
112
+ param_container = parameter_parts[0..-2].reduce(configuration) do |container, part|
113
+ container[part] ||= {}
114
+ end
115
+ param_container[parameter_parts[-1]] = get_latest_key_value(key_name)
116
+ end
117
+
118
+ configuration
119
+ end
120
+
121
+ def get_gcp_key_name(parameter)
122
+ parameter.name.split(GCP_SEPARATOR).last
123
+ end
124
+
125
+ def get_latest_key_value(key_name)
126
+ name = @gcsm.secret_version_path(
127
+ project: @project_id,
128
+ secret: key_name,
129
+ secret_version: 'latest'
130
+ )
131
+ version = @gcsm.access_secret_version(name: name)
132
+ version.payload.data
133
+ end
134
+
135
+ end
136
+ end
137
+ end
data/lib/global/base.rb CHANGED
@@ -1,20 +1,26 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'erb'
4
4
  require 'json'
5
5
 
6
6
  module Global
7
7
  module Base
8
- extend self
9
8
 
10
- attr_writer :environment, :config_directory, :namespace, :except, :only
9
+ extend self
11
10
 
12
11
  def configure
13
12
  yield self
14
13
  end
15
14
 
16
15
  def configuration
17
- @configuration ||= load_configuration(config_directory, environment)
16
+ raise 'Backend must be defined' unless @backends
17
+
18
+ @configuration ||= begin
19
+ configuration_hash = @backends.reduce({}) do |configuration, backend|
20
+ configuration.deep_merge(backend.load.with_indifferent_access)
21
+ end
22
+ Configuration.new(configuration_hash)
23
+ end
18
24
  end
19
25
 
20
26
  def reload!
@@ -22,72 +28,46 @@ module Global
22
28
  configuration
23
29
  end
24
30
 
25
- def environment
26
- @environment || raise("environment should be defined")
27
- end
28
-
29
- def config_directory
30
- @config_directory || raise("config_directory should be defined")
31
- end
32
-
33
- def namespace
34
- @namespace ||= 'Global'
35
- end
36
-
37
- def except
38
- @except ||= :all
39
- end
40
-
41
- def only
42
- @only ||= []
43
- end
44
-
45
- def generate_js(options = {})
46
- current_namespace = options[:namespace] || namespace
47
-
48
- js_options = { except: except, only: only }.merge(options)
49
- "window.#{current_namespace} = #{configuration.filter(js_options).to_json}"
31
+ # Add a backend to load configuration from.
32
+ #
33
+ # You can define several backends; they will all be loaded
34
+ # and the configuration hashes will be merged.
35
+ #
36
+ # Configure with either:
37
+ # Global.backend :filesystem, path: 'config', environment: Rails.env
38
+ # or:
39
+ # Global.backend YourConfigurationBackend.new
40
+ #
41
+ # backend configuration classes MUST have a `load` method that returns a configuration Hash
42
+ def backend(backend, options = {})
43
+ @backends ||= []
44
+ if backend.is_a?(Symbol)
45
+ require "global/backend/#{backend}"
46
+ backend_class = Global::Backend.const_get(camel_case(backend.to_s))
47
+ @backends.push backend_class.new(options)
48
+ elsif backend.respond_to?(:load)
49
+ @backends.push backend
50
+ else
51
+ raise 'Backend must be either a Global::Backend class or a symbol'
52
+ end
50
53
  end
51
54
 
52
55
  protected
53
56
 
54
- def load_configuration(dir, env)
55
- config = load_from_file(dir, env)
56
- config.deep_merge!(load_from_directory(dir, env))
57
- Configuration.new(config)
57
+ def respond_to_missing?(method, include_private = false)
58
+ configuration.key?(method) || super
58
59
  end
59
60
 
60
- def load_from_file(dir, env)
61
- config = {}
62
-
63
- if File.exists?(file = "#{dir}.yml")
64
- configurations = YAML::load(ERB.new(IO.read(file)).result)
65
- config = configurations[:default] || configurations["default"] || {}
66
- config.deep_merge!(configurations[env] || {})
67
- end
68
-
69
- config
61
+ def method_missing(method, *args, &block)
62
+ configuration.key?(method) ? configuration.get_configuration_value(method) : super
70
63
  end
71
64
 
72
- def load_from_directory(dir, env)
73
- config = {}
65
+ # from Bundler::Thor::Util.camel_case
66
+ def camel_case(str)
67
+ return str if str !~ /_/ && str =~ /[A-Z]+.*/
74
68
 
75
- if File.directory?(dir)
76
- Dir["#{dir}/*"].each do |entry|
77
- namespace = entry.gsub(/^#{dir}\/?/, '').gsub(/\.yml$/, '')
78
- config.deep_merge!(namespace => load_configuration(entry.gsub(/\.yml$/, ''), env))
79
- end
80
- end
81
-
82
- config
83
- end
84
-
85
- def respond_to_missing?(method, include_private=false)
86
- configuration.key?(method) || super
69
+ str.split('_').map(&:capitalize).join
87
70
  end
88
71
 
89
- def method_missing(method, *args, &block)
90
- configuration.key?(method) ? configuration[method] : super
91
- end
92
72
  end
93
73
  end
@@ -1,15 +1,17 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
4
 
5
5
  module Global
6
6
  class Configuration
7
+
7
8
  extend Forwardable
8
9
 
9
10
  attr_reader :hash
10
11
 
11
- def_delegators :hash, :key?, :has_key?, :include?, :member?, :[], :[]=, :to_hash, :to_json, :inspect
12
-
12
+ def_delegators :hash, :key?, :has_key?, :include?,
13
+ :member?, :[], :[]=, :to_hash, :to_json,
14
+ :inspect, :fetch
13
15
 
14
16
  def initialize(hash)
15
17
  @hash = hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
@@ -17,46 +19,51 @@ module Global
17
19
 
18
20
  def filter(options = {})
19
21
  keys = filtered_keys_list(options)
20
- hash.select{|key, _| keys.include?(key)}
22
+ hash.select { |key, _| keys.include?(key) }
23
+ end
24
+
25
+ def get_configuration_value(key)
26
+ return nil unless key?(key)
27
+
28
+ value = hash[key]
29
+ value.is_a?(Hash) ? Global::Configuration.new(value) : value
21
30
  end
22
31
 
23
32
  private
24
33
 
25
34
  def filtered_keys_list(options)
26
- if options[:except].is_a?(Array)
27
- return hash.keys - options[:except].map(&:to_s)
28
- end
29
-
30
- if options[:only].is_a?(Array)
31
- return hash.keys & options[:only].map(&:to_s)
32
- end
35
+ return hash.keys - options[:except].map(&:to_s) if options[:except].is_a?(Array)
36
+ return hash.keys & options[:only].map(&:to_s) if options[:only].is_a?(Array)
33
37
 
34
38
  return hash.keys if options[:only] == :all
35
39
  return [] if options[:except] == :all
36
- return []
40
+
41
+ []
37
42
  end
38
43
 
39
44
  protected
40
45
 
41
- def respond_to_missing?(method_name, include_private=false)
46
+ def respond_to_missing?(method_name, include_private = false)
42
47
  method = normalize_key_by_method(method_name)
43
- key?(method) || super
48
+ key?(method) || boolean_method?(method) || super
44
49
  end
45
50
 
46
51
  def method_missing(method, *args, &block)
47
52
  method = normalize_key_by_method(method)
48
53
  if key?(method)
49
- value = hash[method]
50
- value.kind_of?(Hash) ? Global::Configuration.new(value) : value
54
+ get_configuration_value(method)
51
55
  else
52
56
  super
53
57
  end
54
58
  end
55
59
 
56
- def normalize_key_by_method(method)
57
- '?' == method.to_s[-1] ? method.to_s[0..-2] : method
60
+ def boolean_method?(method)
61
+ '?' == method.to_s[-1]
58
62
  end
59
63
 
64
+ def normalize_key_by_method(method)
65
+ boolean_method?(method) ? method.to_s[0..-2].to_sym : method
66
+ end
60
67
 
61
68
  end
62
69
  end
@@ -1,5 +1,7 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Global
4
- VERSION = "0.2.2"
4
+
5
+ VERSION = '2.1.0'
6
+
5
7
  end
data/lib/global.rb CHANGED
@@ -1,4 +1,6 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
2
4
  require 'yaml'
3
5
 
4
6
  require 'active_support/core_ext/hash/indifferent_access'
@@ -6,9 +8,10 @@ require 'active_support/core_ext/hash/deep_merge'
6
8
 
7
9
  require 'global/configuration'
8
10
  require 'global/base'
9
- require 'global/engine' if defined?(Rails)
10
11
  require 'global/version'
11
12
 
12
13
  module Global
14
+
13
15
  extend Base
16
+
14
17
  end