anyway_config 2.3.0 → 2.4.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.
@@ -8,6 +8,7 @@ module Anyway # :nodoc:
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::DeepFreeze
10
10
  using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
11
12
 
12
13
  using(Module.new do
13
14
  refine Object do
@@ -26,6 +27,7 @@ module Anyway # :nodoc:
26
27
  RESERVED_NAMES = %i[
27
28
  config_name
28
29
  env_prefix
30
+ as_env
29
31
  values
30
32
  class
31
33
  clear
@@ -47,8 +49,6 @@ module Anyway # :nodoc:
47
49
  __type_caster__
48
50
  ].freeze
49
51
 
50
- ENV_OPTION_EXCLUDE_KEY = :except
51
-
52
52
  class Error < StandardError; end
53
53
 
54
54
  class ValidationError < Error; end
@@ -128,34 +128,14 @@ module Anyway # :nodoc:
128
128
  end
129
129
  end
130
130
 
131
- def required(*names, env: nil)
132
- unknown_names = names - config_attributes
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
133
  raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
134
 
135
- names = filter_by_env(names, env)
136
- required_attributes.push(*names)
137
- end
135
+ return unless Settings.matching_env?(env)
138
136
 
139
- def filter_by_env(names, env)
140
- return names if env.nil? || env.to_s == current_env
141
-
142
- filtered_names = if env.is_a?(Hash)
143
- names_with_exclude_env_option(names, env)
144
- elsif env.is_a?(Array)
145
- names if env.flat_map(&:to_s).include?(current_env)
146
- end
147
-
148
- filtered_names || []
149
- end
150
-
151
- def current_env
152
- Settings.current_environment.to_s
153
- end
154
-
155
- def names_with_exclude_env_option(names, env)
156
- envs = env[ENV_OPTION_EXCLUDE_KEY]
157
- excluded_envs = [envs].flat_map(&:to_s)
158
- names if excluded_envs.none?(current_env)
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
159
139
  end
160
140
 
161
141
  def required_attributes
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -297,7 +283,7 @@ module Anyway # :nodoc:
297
283
  # - SomeModule::Config => "some_module"
298
284
  # - SomeConfig => "some"
299
285
  unless name =~ /^(\w+)(::)?Config$/
300
- raise "Couldn't infer config name, please, specify it explicitly" \
286
+ raise "Couldn't infer config name, please, specify it explicitly " \
301
287
  "via `config_name :my_config`"
302
288
  end
303
289
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -8,14 +8,31 @@ module Anyway
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::Hash
10
10
 
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo: memo)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
11
27
  include Tracing
12
28
 
13
- attr_reader :data, :traces, :type_cast
29
+ attr_reader :data, :traces, :type_cast, :env_container
14
30
 
15
- def initialize(type_cast: AutoCast)
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
16
32
  @type_cast = type_cast
17
33
  @data = {}
18
34
  @traces = {}
35
+ @env_container = env_container
19
36
  end
20
37
 
21
38
  def clear
@@ -42,11 +59,11 @@ module Anyway
42
59
  private
43
60
 
44
61
  def parse_env(prefix)
45
- match_prefix = "#{prefix}_"
46
- ENV.each_pair.with_object({}) do |(key, val), data|
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
47
64
  next unless key.start_with?(match_prefix)
48
65
 
49
- path = key.sub(/^#{prefix}_/, "").downcase
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
50
67
 
51
68
  paths = path.split("__")
52
69
  trace!(:env, *paths, key: key) { data.bury(type_cast.call(val), *paths) }
@@ -19,7 +19,7 @@ module Anyway
19
19
  def initialize(type = :trace, value = UNDEF, **source)
20
20
  @type = type
21
21
  @source = source
22
- @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
22
+ @value = (value == UNDEF) ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
23
23
  end
24
24
 
25
25
  def dig(...)
@@ -35,7 +35,7 @@ module Anyway
35
35
  end
36
36
 
37
37
  target_trace = path.empty? ? self : value.dig(*path)
38
- target_trace.value[key.to_s] = trace
38
+ target_trace.record_key(key.to_s, trace)
39
39
 
40
40
  val
41
41
  end
@@ -54,6 +54,12 @@ module Anyway
54
54
  hash
55
55
  end
56
56
 
57
+ def record_key(key, key_trace)
58
+ @value = Hash.new { |h, k| h[k] = Trace.new(:trace) } unless value.is_a?(::Hash)
59
+
60
+ value[key] = key_trace
61
+ end
62
+
57
63
  def merge!(another_trace)
58
64
  raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
59
65
  raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
data/lib/anyway/config.rb CHANGED
@@ -8,6 +8,7 @@ module Anyway # :nodoc:
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::DeepFreeze
10
10
  using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
11
12
 
12
13
  using(Module.new do
13
14
  refine Object do
@@ -26,6 +27,7 @@ module Anyway # :nodoc:
26
27
  RESERVED_NAMES = %i[
27
28
  config_name
28
29
  env_prefix
30
+ as_env
29
31
  values
30
32
  class
31
33
  clear
@@ -47,8 +49,6 @@ module Anyway # :nodoc:
47
49
  __type_caster__
48
50
  ].freeze
49
51
 
50
- ENV_OPTION_EXCLUDE_KEY = :except
51
-
52
52
  class Error < StandardError; end
53
53
 
54
54
  class ValidationError < Error; end
@@ -128,34 +128,14 @@ module Anyway # :nodoc:
128
128
  end
129
129
  end
130
130
 
131
- def required(*names, env: nil)
132
- unknown_names = names - config_attributes
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
133
  raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
134
 
135
- names = filter_by_env(names, env)
136
- required_attributes.push(*names)
137
- end
135
+ return unless Settings.matching_env?(env)
138
136
 
139
- def filter_by_env(names, env)
140
- return names if env.nil? || env.to_s == current_env
141
-
142
- filtered_names = if env.is_a?(Hash)
143
- names_with_exclude_env_option(names, env)
144
- elsif env.is_a?(Array)
145
- names if env.flat_map(&:to_s).include?(current_env)
146
- end
147
-
148
- filtered_names || []
149
- end
150
-
151
- def current_env
152
- Settings.current_environment.to_s
153
- end
154
-
155
- def names_with_exclude_env_option(names, env)
156
- envs = env[ENV_OPTION_EXCLUDE_KEY]
157
- excluded_envs = [envs].flat_map(&:to_s)
158
- names if excluded_envs.none?(current_env)
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
159
139
  end
160
140
 
161
141
  def required_attributes
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -297,7 +283,7 @@ module Anyway # :nodoc:
297
283
  # - SomeModule::Config => "some_module"
298
284
  # - SomeConfig => "some"
299
285
  unless name =~ /^(\w+)(::)?Config$/
300
- raise "Couldn't infer config name, please, specify it explicitly" \
286
+ raise "Couldn't infer config name, please, specify it explicitly " \
301
287
  "via `config_name :my_config`"
302
288
  end
303
289
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "anyway/ext/hash"
5
+
6
+ using Anyway::Ext::Hash
7
+
8
+ module Anyway
9
+ class EJSONParser
10
+ attr_reader :bin_path
11
+
12
+ def initialize(bin_path = "ejson")
13
+ @bin_path = bin_path
14
+ end
15
+
16
+ def call(file_path)
17
+ return unless File.exist?(file_path)
18
+
19
+ raw_content = nil
20
+
21
+ stdout, stderr, status = Open3.capture3("#{bin_path} decrypt #{file_path}")
22
+
23
+ if status.success?
24
+ raw_content = JSON.parse(stdout.chomp)
25
+ else
26
+ Kernel.warn "Failed to decrypt #{file_path}: #{stderr}"
27
+ end
28
+
29
+ return unless raw_content
30
+
31
+ raw_content.deep_transform_keys do |key|
32
+ if key[0] == "_"
33
+ key[1..]
34
+ else
35
+ key
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/anyway/env.rb CHANGED
@@ -8,14 +8,31 @@ module Anyway
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::Hash
10
10
 
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo:)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
11
27
  include Tracing
12
28
 
13
- attr_reader :data, :traces, :type_cast
29
+ attr_reader :data, :traces, :type_cast, :env_container
14
30
 
15
- def initialize(type_cast: AutoCast)
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
16
32
  @type_cast = type_cast
17
33
  @data = {}
18
34
  @traces = {}
35
+ @env_container = env_container
19
36
  end
20
37
 
21
38
  def clear
@@ -42,11 +59,11 @@ module Anyway
42
59
  private
43
60
 
44
61
  def parse_env(prefix)
45
- match_prefix = "#{prefix}_"
46
- ENV.each_pair.with_object({}) do |(key, val), data|
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
47
64
  next unless key.start_with?(match_prefix)
48
65
 
49
- path = key.sub(/^#{prefix}_/, "").downcase
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
50
67
 
51
68
  paths = path.split("__")
52
69
  trace!(:env, *paths, key:) { data.bury(type_cast.call(val), *paths) }
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Ext
5
+ # Convert Hash with mixed array and hash values to an
6
+ # array of paths.
7
+ module FlattenNames
8
+ refine ::Array do
9
+ def flatten_names(prefix, buf)
10
+ if empty?
11
+ buf << :"#{prefix}"
12
+ return buf
13
+ end
14
+
15
+ each_with_object(buf) do |name, acc|
16
+ if name.is_a?(::Symbol)
17
+ acc << :"#{prefix}.#{name}"
18
+ else
19
+ name.flatten_names(prefix, acc)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ refine ::Hash do
26
+ def flatten_names(prefix = nil, buf = [])
27
+ each_with_object(buf) do |(k, v), acc|
28
+ parent = prefix ? "#{prefix}.#{k}" : k
29
+ v.flatten_names(parent, acc)
30
+ end
31
+ end
32
+ end
33
+
34
+ using self
35
+ end
36
+ end
37
+ end
@@ -21,11 +21,18 @@ module Anyway
21
21
 
22
22
  last_key = path.pop
23
23
  hash = path.reduce(self) do |hash, k|
24
- hash[k] = {} unless hash.key?(k)
24
+ hash[k] = {} unless hash.key?(k) && hash[k].is_a?(::Hash)
25
25
  hash[k]
26
26
  end
27
+
27
28
  hash[last_key] = val
28
29
  end
30
+
31
+ def deep_transform_keys(&block)
32
+ each_with_object({}) do |(key, value), result|
33
+ result[yield(key)] = value.is_a?(::Hash) ? value.deep_transform_keys(&block) : value
34
+ end
35
+ end
29
36
  end
30
37
 
31
38
  using self
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Ext
5
+ # Add simple safe_constantize method to String
6
+ module StringConstantize
7
+ refine String do
8
+ def safe_constantize
9
+ names = split("::")
10
+
11
+ return nil if names.empty?
12
+
13
+ # Remove the first blank element in case of '::ClassName' notation.
14
+ names.shift if names.size > 1 && names.first.empty?
15
+
16
+ names.inject(Object) do |constant, name|
17
+ break if constant.nil?
18
+ constant.const_get(name, false) if constant.const_defined?(name, false)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ module Anyway
8
+ using RubyNext
9
+
10
+ module Loaders
11
+ class Doppler < Base
12
+ class RequestError < StandardError; end
13
+
14
+ class << self
15
+ attr_accessor :download_url
16
+ attr_writer :token
17
+
18
+ def token
19
+ @token || ENV["DOPPLER_TOKEN"]
20
+ end
21
+ end
22
+
23
+ self.download_url = "https://api.doppler.com/v3/configs/config/secrets/download"
24
+
25
+ def call(env_prefix:, **_options)
26
+ env_payload = parse_doppler_response(url: Doppler.download_url, token: Doppler.token)
27
+
28
+ env = ::Anyway::Env.new(type_cast: ::Anyway::NoCast, env_container: env_payload)
29
+
30
+ env.fetch_with_trace(env_prefix).then do |(conf, trace)|
31
+ Tracing.current_trace&.merge!(trace)
32
+ conf
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def parse_doppler_response(url:, token:)
39
+ response = fetch_doppler_config(url, token)
40
+
41
+ unless response.is_a?(Net::HTTPSuccess)
42
+ raise RequestError, "#{response.code} #{response.message}"
43
+ end
44
+
45
+ JSON.parse(response.read_body)
46
+ end
47
+
48
+ def fetch_doppler_config(url, token)
49
+ uri = URI.parse(url)
50
+ raise "Doppler token is required to load configuration from Doppler" if token.nil?
51
+
52
+ http = Net::HTTP.new(uri.host, uri.port)
53
+ http.use_ssl = true if uri.scheme == "https"
54
+
55
+ request = Net::HTTP::Get.new(uri)
56
+ request["Accept"] = "application/json"
57
+ request["Authorization"] = "Bearer #{token}"
58
+
59
+ http.request(request)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway/ejson_parser"
4
+
5
+ module Anyway
6
+ module Loaders
7
+ class EJSON < Base
8
+ class << self
9
+ attr_accessor :bin_path
10
+ end
11
+
12
+ self.bin_path = "ejson"
13
+
14
+ def call(name:, ejson_parser: Anyway::EJSONParser.new(EJSON.bin_path), **_options)
15
+ configs = []
16
+
17
+ rel_config_paths.each do |rel_config_path|
18
+ secrets_hash, rel_path =
19
+ extract_hash_from_rel_config_path(
20
+ ejson_parser: ejson_parser,
21
+ rel_config_path: rel_config_path
22
+ )
23
+
24
+ next unless secrets_hash
25
+
26
+ config_hash = secrets_hash[name]
27
+
28
+ next unless config_hash.is_a?(Hash)
29
+
30
+ configs <<
31
+ trace!(:ejson, path: rel_path) do
32
+ config_hash
33
+ end
34
+ end
35
+
36
+ return {} if configs.empty?
37
+
38
+ configs.inject do |result_config, next_config|
39
+ Utils.deep_merge!(result_config, next_config)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def rel_config_paths
46
+ chain = [environmental_rel_config_path]
47
+
48
+ chain << "secrets.local.ejson" if use_local?
49
+
50
+ chain
51
+ end
52
+
53
+ def environmental_rel_config_path
54
+ if Settings.current_environment
55
+ # if environment file is absent, then take data from the default one
56
+ [
57
+ "#{Settings.current_environment}/secrets.ejson",
58
+ default_rel_config_path
59
+ ]
60
+ else
61
+ default_rel_config_path
62
+ end
63
+ end
64
+
65
+ def default_rel_config_path
66
+ "secrets.ejson"
67
+ end
68
+
69
+ def extract_hash_from_rel_config_path(ejson_parser:, rel_config_path:)
70
+ rel_config_path = [rel_config_path] unless rel_config_path.is_a?(Array)
71
+
72
+ rel_config_path.each do |rel_conf_path|
73
+ rel_path = "config/#{rel_conf_path}"
74
+ abs_path = "#{Settings.app_root}/#{rel_path}"
75
+
76
+ result = ejson_parser.call(abs_path)
77
+
78
+ return [result, rel_path] if result
79
+ end
80
+
81
+ nil
82
+ end
83
+ end
84
+ end
85
+ end
@@ -53,13 +53,13 @@ module Anyway
53
53
  # the interface when no config file is present.
54
54
  begin
55
55
  if defined?(ERB)
56
- ::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {} # rubocop:disable Security/YAMLLoad
56
+ ::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {}
57
57
  else
58
58
  ::YAML.load_file(path, aliases: true) || {}
59
59
  end
60
60
  rescue ArgumentError
61
61
  if defined?(ERB)
62
- ::YAML.load(ERB.new(File.read(path)).result) || {} # rubocop:disable Security/YAMLLoad
62
+ ::YAML.load(ERB.new(File.read(path)).result) || {}
63
63
  else
64
64
  ::YAML.load_file(path) || {}
65
65
  end
@@ -75,3 +75,5 @@ end
75
75
  require "anyway/loaders/base"
76
76
  require "anyway/loaders/yaml"
77
77
  require "anyway/loaders/env"
78
+ require "anyway/loaders/doppler"
79
+ require "anyway/loaders/ejson"
@@ -18,6 +18,8 @@ module Anyway
18
18
 
19
19
  return unless ::Rails.root.join(val).exist?
20
20
 
21
+ return if val == autoload_static_config_path
22
+
21
23
  autoloader&.unload
22
24
 
23
25
  @autoload_static_config_path = val
@@ -80,6 +80,20 @@ module Anyway
80
80
  def default_environmental_key?
81
81
  !default_environmental_key.nil?
82
82
  end
83
+
84
+ def matching_env?(env)
85
+ return true if env.nil? || env.to_s == current_environment
86
+
87
+ if env.is_a?(::Hash)
88
+ envs = env[:except]
89
+ excluded_envs = [envs].flat_map(&:to_s)
90
+ excluded_envs.none?(current_environment)
91
+ elsif env.is_a?(::Array)
92
+ env.flat_map(&:to_s).include?(current_environment)
93
+ else
94
+ false
95
+ end
96
+ end
83
97
  end
84
98
 
85
99
  # By default, use local files only in development (that's the purpose if the local files)