diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 71266f1..a17ab58 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -14,7 +14,7 @@
<%= tag :meta, name: :viewport, content: 'width=device-width,initial-scale=1' %>
<% if Rails.env.development? %>
<%= tag :meta, name: 'turbo-prefetch', content: false %>
- <%= tag :meta, name: 'stimulus-debug', content: Hot::Constants.stimulus_debug %>
+ <%= tag :meta, name: 'stimulus-debug', content: Live::Constants::STIMULUS_DEBUG %>
<% end%>
diff --git a/app/views/scores/index.html.erb b/app/views/scores/index.html.erb
index edf3391..8fcdb77 100644
--- a/app/views/scores/index.html.erb
+++ b/app/views/scores/index.html.erb
@@ -2,6 +2,9 @@
List of Scores
+ACTION_CONTROLLER = <%=Live::Constants::ACTION_CONTROLLER%>
+ACTION_VIEW = <%=Live::Constants::ACTION_VIEW%>
+
<%= link_to "New score", new_score_path, class: "border rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
diff --git a/config/application.rb b/config/application.rb
index 5d21136..a4a7714 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -15,26 +15,10 @@ module EasyGoingRails
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.2
- # Please, add to the `ignore` list any other `lib` subdirectories that do
- # not contain `.rb` files, or that should not be reloaded or eager loaded.
- # Common ones are `templates`, `generators`, or `middleware`, for example.
- # config.autoload_lib(ignore: %w[assets tasks formatters hot_constants monkey_patches])
- # config.autoload_lib(ignore: %w[assets tasks formatters monkey_patches])
- config.autoload_lib(ignore: %w[assets tasks])
+ # ignoring monkey_patches because MonkeyPatcher would carry out
+ config.autoload_lib(ignore: %w[assets tasks monkey_patches])
# main application title defined from current module name, see module above
config.application_title = module_parent.to_s.titleize
-
- # Configuration for the application, engines, and railties goes here.
- #
- # These settings can be overridden in specific environments using the files
- # in config/environments, which are processed later.
- #
- # config.time_zone = "Central Time (US & Canada)"
- # config.eager_load_paths << Rails.root.join("extras")
-
- # Customized Semantic Logger
- config.rails_semantic_logger.semantic = false
- config.rails_semantic_logger.add_file_appender = false
end
end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 1646fe6..a05f34d 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -77,19 +77,8 @@ Rails.application.configure do # rubocop:disable Metrics/BlockLength
routes.default_url_options[:port] = ARGV[1] # ie: Procfile.dev --port PORT
routes.default_url_options[:host] = '127.0.0.1'
- if Rails.application.console?
- config.after_initialize do
- formatter = Semantic::BasicFormatter.new
- SemanticLogger.add_appender(io: $stdout, formatter:)
-
- # FIXME: filter is useful!
- # SemanticLogger.add_appender(io: $stdout,
- # formatter:,
- # filter: ->(log) { !formatter.reject(log) })
- end
- elsif Rails.application.server?
- config.after_initialize do
- config.dev_loader = Semantic::DevLoader.new('toto1')
- end
+ Rails.configuration.after_initialize do
+ # SemanticLogger.add_appender(io: $stdout, formatter: Semantic::BasicFormatter.new)
+ SemanticLogger.add_appender(io: $stdout, formatter: Semantic::FancyFormatter.new)
end
end
diff --git a/config/initializers/hot_changes.rb b/config/initializers/hot_changes.rb
deleted file mode 100644
index 2e997e1..0000000
--- a/config/initializers/hot_changes.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-return unless Rails.application.server?
-
-Rails.application.config.after_initialize do
- Hot::Constants.on_change(:log_active_record) { |bool| ActiveRecord::Base.logger.level = bool ? :debug : :fatal }
- Hot::Constants.on_change(:log_action_view) do |bool|
- ActionView::Base.logger.level = bool ? :debug : :fatal
- Rails.application.config.dev_loader.launch
- end
-end
diff --git a/config/initializers/monkey_patcher.rb b/config/initializers/monkey_patcher.rb
deleted file mode 100644
index 467ead6..0000000
--- a/config/initializers/monkey_patcher.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-return unless Rails.application.server?
-
-# puts 'MonkeyPatcher runs:'
-patches = Dir.glob(Rails.root.join('lib', 'monkey_patches', '**', '*.rb'))
-patches.each do |file|
- # puts "🐵 patching... #{Pathname.new(file).relative_path_from Rails.root}"
- require file
-end
-
-# puts case patches.count
-# when 0 then 'No patch found'
-# when 1 then '1 successful patch applied'
-# else "#{patches.count} successful patches applied"
-# end
diff --git a/config/initializers/monkey_patches.rb b/config/initializers/monkey_patches.rb
new file mode 100644
index 0000000..638d02e
--- /dev/null
+++ b/config/initializers/monkey_patches.rb
@@ -0,0 +1,12 @@
+return unless Rails.application.server? || Rails.application.console?
+
+require_relative '../../lib/monkey_patches/monkey_patcher'
+
+MonkeyPatcher.run do |patch|
+ case patch
+ when 'action_dispatch/middleware/debug_exceptions.rb' then Rails.application.server?
+ when 'rails_live_reload/watcher.rb' then Rails.application.server? && Rails.env.development?
+ when /^semantic/ then true
+ else false
+ end
+end
diff --git a/config/initializers/rails_live_reload.rb b/config/initializers/rails_live_reload.rb
index cfbf84f..0cac7a9 100644
--- a/config/initializers/rails_live_reload.rb
+++ b/config/initializers/rails_live_reload.rb
@@ -1,16 +1,11 @@
return unless defined?(RailsLiveReload) && Rails.env.development?
+# RailsLiveReload
RailsLiveReload.configure do |config|
- # HOT Constants
- config.watch(%r{/\.env$}, reload: :always)
- config.watch(%r{/\.env\.sample$})
- # config.watch(%r{/config/initializers/hot_changes.rb$})
-
- # USEFUL for tailwind changes!!!!
- config.watch %r{app/assets/builds/tailwind.css}, reload: :always
-
- # Rk: prevent any reload from files ending with '.tailwind.css'
+ # Tailwind CSS
+ # First rule prevents reloading from asset files ending with '.tailwind.css'
config.watch %r{(app|vendor)/(assets|javascript)/.+\.(css|js|html|png|jpg)(?")
- singleton_class.undef_method(dkey)
- @hot_definitions.delete(key)
- end
-
- definitions_to_add = new_definitions.except(*@hot_definitions.keys)
- definitions_to_add.each { |key, value| define_method(:add, key, value) }
- end
-
- def load_values
- logger.debug('--load_values')
-
- new_env = Dotenv.parse
- constants_to_delete = HOTENV.except(*new_env.keys)
- constants_to_delete.each do |name, _|
- # FIXME: default should read default and type from @old_definitions
- type, default = @old_definitions[name]
- logger.info("constant <#{name}> reverts to default value <#{default}> of type <#{type}>")
- end
-
- constants_to_add = new_env.except(*HOTENV.keys)
- constants_to_add.each do |constant|
- logger.info("constant to add <#{constant}>")
- end
-
- HOTENV.replace new_env
- LISTENERS.each_pair { |k, b| perform_change(k, b) }
- end
-
- def on_change(key, &block)
- LISTENERS[key.downcase.to_sym] = block
- perform_change(key, block)
- end
-
- private
-
- def define_method(mode, key, value)
- dkey = key.downcase
- method = infer_method_from_value(value)
-
- inferred_type = method.name.to_s.split('_')[1]
- logger.info("#{mode} method <#{dkey}> of type <#{inferred_type}> with default value <#{value}> ")
-
- singleton_class.define_method(dkey) { method.call(key, value) }
- @hot_definitions.store(key, dkey)
- end
-
- def perform_change(key, block)
- old_value = nil # TODO: remember last previous value
- new_value = method(key).call
- block.call(new_value, old_value)
- end
-
- def load_boolean(key, default) = HOTENV.fetch(key, default).to_s.downcase == 'true'
- def load_integer(key, default) = HOTENV.fetch(key, default).to_i
- def load_string(key, default) = HOTENV.fetch(key, default)
-
- def infer_method_from_value(value)
- case value.downcase
- when /^(true|false)$/ then method(:load_boolean)
- when /^\d+$/ then method(:load_integer)
- else method(:load_string)
- end
- end
- end
-
- initialize # done once on first require, cause this is just a module (not a class!)
- end
-end
diff --git a/lib/hot/live.rb b/lib/hot/live.rb
deleted file mode 100644
index ebce05f..0000000
--- a/lib/hot/live.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module Hot
- # Hot Live constants
- class Live
- def initialize
- puts 'Hot Live constants initialized'
- end
- end
-end
diff --git a/lib/live/constants.rb b/lib/live/constants.rb
new file mode 100644
index 0000000..eded9fa
--- /dev/null
+++ b/lib/live/constants.rb
@@ -0,0 +1,13 @@
+module Live
+ # My live constants
+ module Constants
+ extend Definable
+
+ STIMULUS_DEBUG = boolean false
+ ACTION_VIEW = boolean true
+ ACTION_CONTROLLER = boolean true
+
+ MY_INTEGER = integer 8
+ MY_STRING = string 'titi'
+ end
+end
diff --git a/lib/live/definable.rb b/lib/live/definable.rb
new file mode 100644
index 0000000..50909da
--- /dev/null
+++ b/lib/live/definable.rb
@@ -0,0 +1,118 @@
+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
+
+ return unless RailsLiveReload.watcher
+
+ FileUtils.touch(MAIN_CSS) # 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
diff --git a/lib/monkey_patches/monkey_patcher.rb b/lib/monkey_patches/monkey_patcher.rb
new file mode 100644
index 0000000..9c304be
--- /dev/null
+++ b/lib/monkey_patches/monkey_patcher.rb
@@ -0,0 +1,17 @@
+class MonkeyPatcher
+ class << self
+ MONKEY_PATCHES = Rails.root.join('lib', 'monkey_patches')
+ def run(&)
+ patches = Dir.glob(MONKEY_PATCHES.join('**', '*.rb'))
+ patches.each do |patch|
+ file = Pathname.new(patch).relative_path_from(MONKEY_PATCHES).to_s
+ next if file == 'monkey_patcher.rb' # filter off its own file!
+
+ if !block_given? || yield(file)
+ puts "🐵 patching... #{file}"
+ require file
+ end
+ end
+ end
+ end
+end
diff --git a/lib/monkey_patches/rails_live_reload/watcher.rb b/lib/monkey_patches/rails_live_reload/watcher.rb
index 06dfd35..7e1a17f 100644
--- a/lib/monkey_patches/rails_live_reload/watcher.rb
+++ b/lib/monkey_patches/rails_live_reload/watcher.rb
@@ -1,9 +1,4 @@
module RailsLiveReload
- ENV_FILE = Rails.root.join('.env').to_s
- ENV_SAMPLE_FILE = Rails.root.join('.env.sample').to_s
- INITIALIZER = Rails.root.join('config/initializers/hot_changes.rb').to_s
- CHECKSUMS = {} # rubocop:disable Style/MutableConstant
-
# MonkeyPath Watcher
class Watcher
def initialize
@@ -19,29 +14,5 @@ module RailsLiveReload
start_socket
start_listener
end
-
- def reload_all
- before_reload(files)
- data = { event: RailsLiveReload::INTERNAL[:socket_events][:reload], files: }.to_json
- @sockets.each { |socket, _| socket.puts data } # rubocop:disable Style/HashEachMethods
- end
-
- private
-
- def before_reload(files)
- perform_when_change(files, ENV_SAMPLE_FILE) { Hot::Constants.load_definitions }
- perform_when_change(files, ENV_FILE) { Hot::Constants.load_values }
- perform_when_change(files, INITIALIZER) { load Rails.root.join('config', 'initializers', 'hot_changes.rb') }
- end
-
- def perform_when_change(files, key, &)
- return unless files.include?(key)
-
- current_checksum = files[key]
- return unless current_checksum != CHECKSUMS[key]
-
- yield
- CHECKSUMS[key] = current_checksum
- end
end
end
diff --git a/lib/semantic/basic_formatter.rb b/lib/semantic/basic_formatter.rb
index bd34053..c15ab67 100644
--- a/lib/semantic/basic_formatter.rb
+++ b/lib/semantic/basic_formatter.rb
@@ -4,7 +4,6 @@ module Semantic
ANSI_REVERSED_WARNING = "\e[0;30;43m".freeze
ANSI_REVERSED_ERROR = "\e[1;30;41m".freeze
ANSI_GRAY = "\e[90m".freeze
- # Return the complete log level name in uppercase
def initialize
super(time_format: '%H:%M:%S',
@@ -75,7 +74,8 @@ module Semantic
self.logger = logger
if @time_format
- [time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(' ')
+ [time, level, process_info, tags, named_tags, duration, name, message, payload,
+ exception].compact.join(' ')
else
[tags, named_tags, duration, name, message, payload, exception].compact.join(' ')
end
diff --git a/lib/semantic/dev_loader.rb b/lib/semantic/dev_loader.rb
deleted file mode 100644
index 8a38b3b..0000000
--- a/lib/semantic/dev_loader.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-module Semantic
- # use the Zeitwerk autoloader to reattach_appender for development autoreloading feature
- class DevLoader
- def initialize(session_key)
- @session_key = session_key
- @subscribers = {}
-
- RailsSemanticLogger::ActionController::LogSubscriber.logger.level = :fatal # useful for remanent Rack::Log started
-
- launch
- end
-
- def launch
- once_and_reload do
- append_ansi_formatter
-
- Semantic::NotificationUtil.clear_subscribers(/\.action_controller$/)
- Semantic::NotificationUtil.clear_subscribers(/\.action_view$/)
- reset_subscribers
-
- register_action_controller
- register_action_view
- end
- end
-
- private
-
- def once_and_reload(&)
- yield
- Rails.autoloaders.main.on_load('ApplicationController', &)
- end
-
- def append_ansi_formatter
- SemanticLogger.clear_appenders!
- formatter = Semantic::AnsiFormatter.new
- SemanticLogger.add_appender(io: $stdout,
- formatter:,
- filter: ->(log) { !formatter.reject(log) })
- end
-
- def register_action_controller
- sub_instance = Semantic::Subscribers::ActionController.new(@session_key)
- register_hook(sub_instance, :start_processing)
- register_hook(sub_instance, :process_action, :finish_processing)
- register_hook(sub_instance, :redirect_to)
- %i[send_file send_data halted_callback unpermitted_parameters send_stream write_fragment
- read_fragment expire_fragment exist_fragment?].each do |hook|
- register_hook(sub_instance, hook, :any_hook)
- end
- end
-
- def register_action_view
- sub_instance = Semantic::Subscribers::ActionView.new
- %i[render_template render_partial render_collection render_layout].each do |hook|
- register_hook(sub_instance, hook)
- end
- end
-
- def register_hook(sub_instance, hook, method = hook)
- @subscribers[sub_instance.class] ||= []
- @subscribers[sub_instance.class] << ActiveSupport::Notifications.subscribe("#{hook}.#{sub_instance.event_group}") do |event|
- sub_instance.send(method, event)
- end
- end
-
- def reset_subscribers
- return if @subscribers.empty?
-
- @subscribers.each_pair do |clazz, subs|
- # puts "reset #{subs.size} subscribers for class <#{clazz}>"
- subs.each { |sub| ActiveSupport::Notifications.unsubscribe(sub) }
- subs.clear
- end
- end
- end
-end
diff --git a/lib/semantic/ansi_dimensions.rb b/lib/semantic/fancy_dimensions.rb
similarity index 95%
rename from lib/semantic/ansi_dimensions.rb
rename to lib/semantic/fancy_dimensions.rb
index cb0620f..307e0e1 100644
--- a/lib/semantic/ansi_dimensions.rb
+++ b/lib/semantic/fancy_dimensions.rb
@@ -2,7 +2,7 @@ require 'ostruct'
module Semantic
# extra dimensions for customizing the logging format
- module AnsiDimensions
+ module FancyDimensions
def self.new(rails: '╣x╠', before: 0, after: 0, terminus: false)
OpenStruct.new(rails:, before:, after:, terminus:) # rubocop:disable Style/OpenStructUse
end
diff --git a/lib/semantic/ansi_formatter.rb b/lib/semantic/fancy_formatter.rb
similarity index 98%
rename from lib/semantic/ansi_formatter.rb
rename to lib/semantic/fancy_formatter.rb
index 74ab1eb..54ceeb5 100644
--- a/lib/semantic/ansi_formatter.rb
+++ b/lib/semantic/fancy_formatter.rb
@@ -4,7 +4,7 @@ require 'json'
module Semantic
# wraps meanwhile takes care of ansi colors
- class AnsiFormatter < SemanticLogger::Formatters::Color
+ class FancyFormatter < SemanticLogger::Formatters::Color
include AnsiColors
TAG_NONE = ''.freeze
@@ -179,7 +179,7 @@ module Semantic
clazz = colorize("#{exc.class}\n", color_map[:fatal])
message = colorize(exc.message.chomp(''), color_map[:error])
- backtrace = stackisize(Rails.backtrace_cleaner.clean(exc.backtrace)) # TODO: backtrace_cleaner might be optionally disable from HotConstant
+ backtrace = stackisize(Rails.backtrace_cleaner.clean(exc.backtrace)) # TODO: backtrace_cleaner might be optionally disable from Live::Constant
"#{clazz}#{message}#{backtrace}"
end
diff --git a/lib/semantic/notification_util.rb b/lib/semantic/notification_util.rb
deleted file mode 100644
index 2c7c4b5..0000000
--- a/lib/semantic/notification_util.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module Semantic
- module NotificationUtil
- class << self
- # pattern could be either a string 'start_processing.action_controller' or a regex /\.action_controller$/
- # FIXME: weird behaviour, order impact!!!!
- # For instance:
- # OK
- # NotificationUtil.clear_subscribers(/\.action_controller$/)
- # NotificationUtil.clear_subscribers(/\.action_view$/)
- # NOPE
- # NotificationUtil.clear_subscribers(/\.action_view$/)
- # NotificationUtil.clear_subscribers(/\.action_controller$/)
- def clear_subscribers(pattern)
- ActiveSupport::LogSubscriber.subscribers.each { |sub| unattach(sub, pattern) }
- end
-
- private
-
- def subscriber_patterns(subscriber)
- subscriber.patterns.respond_to?(:keys) ? subscriber.patterns.keys : subscriber.patterns
- end
-
- def unattach(subscriber, pattern)
- subscriber_patterns(subscriber).each do |sub_pattern|
- ActiveSupport::Notifications.notifier.listeners_for(sub_pattern).each do |sub|
- next unless sub.instance_variable_get(:@delegate) == subscriber
- next unless pattern.match(sub_pattern)
-
- puts "FOUND subscriber=#{subscriber} for sub_pattern=#{sub_pattern} with logger #{subscriber.logger.name}"
- puts subscriber.class.module_parent.const_source_location(subscriber.class.to_s)&.first
-
- ActiveSupport::Notifications.unsubscribe(sub)
- end
- end
- # ActiveSupport::LogSubscriber.subscribers.delete(subscriber)
- end
- end
- end
-end