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.

118 lines
4.0 KiB

  1. require 'dotenv'
  2. $definable_thread_group ||= ThreadGroup.new
  3. module Live
  4. # offers typed constant defintions with default value, by using lots of introspecting...
  5. module Definable
  6. MAIN_CSS = 'app/assets/stylesheets/application.css'.freeze # useful to trigger :reload_all from RailsLiveReload
  7. def integer(default = 0) = define_type_from_callee(caller[0], :integer, default)
  8. def boolean(default = true) = define_type_from_callee(caller[0], :boolean, default) # rubocop:disable Style/OptionalBooleanParameter
  9. def string(default = '') = define_type_from_callee(caller[0], :string, default)
  10. def reload_from_env
  11. logger.debug('reload from env')
  12. @env_values = Dotenv.parse(*Dotenv::Rails.files)
  13. changes = 0
  14. prefix = 'Constant'
  15. env_values.each_pair do |constant, raw|
  16. next unless definitions.include?(constant)
  17. value = typed_value(definitions[constant][:type], raw, definitions[constant][:default])
  18. next unless value != definitions[constant][:value]
  19. define_value(constant, value)
  20. logger.warn "#{prefix} overriden from environment:#{Semantic::AnsiColors::CLEAR} #{constant} = #{value.ai}"
  21. changes += 1
  22. end
  23. definitions.except(*env_values.keys).each_pair do |constant, options|
  24. default = options[:default]
  25. next unless options[:value] != default
  26. define_value(constant, default)
  27. logger.warn "#{prefix} restored:#{Semantic::AnsiColors::CLEAR} #{constant} = #{default.ai}"
  28. changes += 1
  29. end
  30. return unless changes.positive?
  31. # TODO: ...
  32. # changes=[]
  33. # changes << {kind: :overriden, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false}
  34. # changes << {kind: :restored, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false}
  35. # ActiveSupport::Notifications.instrument 'rolling.live_constant', this: changes
  36. return unless RailsLiveReload.watcher
  37. FileUtils.touch(MAIN_CSS) # triggering RailsLiveReload
  38. end
  39. private
  40. def logger = @logger ||= SemanticLogger[self]
  41. def env_values = @env_values ||= Dotenv.parse
  42. def definitions = @definitions ||= {}
  43. def define_value(constant, value)
  44. definitions[constant][:value] = value
  45. remove_const(constant)
  46. const_set(constant, value)
  47. end
  48. # origin (or caller[0]) helps fetching the constant name from source code introspection
  49. def define_type_from_callee(origin, type, default)
  50. @@class_origin ||= self # rubocop:disable Style/ClassVars
  51. @listener ||= start_listener
  52. file, line = origin.split(':')
  53. constant = introspect_constant_from_file(file, line.to_i - 1)
  54. raw_value = env_values.fetch(constant, nil)
  55. value = typed_value(type, raw_value, default)
  56. definitions[constant] = { type:, default:, value: }
  57. # logger.debug('new definitions', definitions)
  58. value
  59. end
  60. def start_listener
  61. $definable_thread_group.list.each(&:kill)
  62. $definable_thread_group.add(Thread.new do
  63. listener = Listen.to(Rails.root, only: /^\.env\.?/) do
  64. @@class_origin.reload_from_env
  65. rescue StandardError
  66. nil
  67. end
  68. listener.start
  69. end)
  70. end
  71. def typed_value(type, raw, default)
  72. return default if raw.nil?
  73. case type
  74. when :integer then raw.to_i
  75. when :boolean then raw.upcase == 'TRUE'
  76. else raw
  77. end
  78. end
  79. # returns current directory of this source code
  80. def dir_source_location
  81. return @dir_source_location if defined?(@dir_source_location)
  82. *paths, _ = Live.const_source_location(:Definable).first.split('/')
  83. @dir_source_location = paths.join('/')
  84. end
  85. def introspect_constant_from_file(file, line)
  86. *dir_file, _ = file.split('/')
  87. dir_file = dir_file.join('/')
  88. raise "unexpected directory: #{dir_file} != #{dir_source_location}" unless dir_file == dir_source_location
  89. @lines ||= File.readlines(file) # cached source code
  90. @lines[line].match(/\s*(.\w+)/)[1] # TODO: should be uppercase!
  91. end
  92. end
  93. end