You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

116 lines
4.0 KiB

require 'dotenv'
$definable_thread_group ||= ThreadGroup.new
module Live
# offers typed constant defintions with default value, by using lots of introspecting...
module Definable
MAIN_CSS = 'app/assets/stylesheets/application.css'.freeze # useful to trigger :reload_all from RailsLiveReload
def integer(default = 0) = define_type_from_callee(caller[0], :integer, default)
def boolean(default = true) = define_type_from_callee(caller[0], :boolean, default) # rubocop:disable Style/OptionalBooleanParameter
def string(default = '') = define_type_from_callee(caller[0], :string, default)
def reload_from_env
logger.debug('reload from env')
@env_values = Dotenv.parse(*Dotenv::Rails.files)
changes = 0
prefix = 'Constant'
env_values.each_pair do |constant, raw|
next unless definitions.include?(constant)
value = typed_value(definitions[constant][:type], raw, definitions[constant][:default])
next unless value != definitions[constant][:value]
define_value(constant, value)
logger.warn "#{prefix} overriden from environment:#{Semantic::AnsiColors::CLEAR} #{constant} = #{value.ai}"
changes += 1
end
definitions.except(*env_values.keys).each_pair do |constant, options|
default = options[:default]
next unless options[:value] != default
define_value(constant, default)
logger.warn "#{prefix} restored:#{Semantic::AnsiColors::CLEAR} #{constant} = #{default.ai}"
changes += 1
end
return unless changes.positive?
# TODO: ...
# changes=[]
# changes << {kind: :overriden, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false}
# changes << {kind: :restored, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false}
# ActiveSupport::Notifications.instrument 'rolling.live_constant', this: changes
FileUtils.touch(MAIN_CSS) if defined?(RailsLiveReload) # triggering RailsLiveReload
end
private
def logger = @logger ||= SemanticLogger[self]
def env_values = @env_values ||= Dotenv.parse
def definitions = @definitions ||= {}
def define_value(constant, value)
definitions[constant][:value] = value
remove_const(constant)
const_set(constant, value)
end
# origin (or caller[0]) helps fetching the constant name from source code introspection
def define_type_from_callee(origin, type, default)
@@class_origin ||= self # rubocop:disable Style/ClassVars
@listener ||= start_listener
file, line = origin.split(':')
constant = introspect_constant_from_file(file, line.to_i - 1)
raw_value = env_values.fetch(constant, nil)
value = typed_value(type, raw_value, default)
definitions[constant] = { type:, default:, value: }
# logger.debug('new definitions', definitions)
value
end
def start_listener
$definable_thread_group.list.each(&:kill)
$definable_thread_group.add(Thread.new do
listener = Listen.to(Rails.root, only: /^\.env\.?/) do
@@class_origin.reload_from_env
rescue StandardError
nil
end
listener.start
end)
end
def typed_value(type, raw, default)
return default if raw.nil?
case type
when :integer then raw.to_i
when :boolean then raw.upcase == 'TRUE'
else raw
end
end
# returns current directory of this source code
def dir_source_location
return @dir_source_location if defined?(@dir_source_location)
*paths, _ = Live.const_source_location(:Definable).first.split('/')
@dir_source_location = paths.join('/')
end
def introspect_constant_from_file(file, line)
*dir_file, _ = file.split('/')
dir_file = dir_file.join('/')
raise "unexpected directory: #{dir_file} != #{dir_source_location}" unless dir_file == dir_source_location
@lines ||= File.readlines(file) # cached source code
@lines[line].match(/\s*(.\w+)/)[1] # TODO: should be uppercase!
end
end
end