require 'securerandom' module Ecoportal module API module Common module Content module ClassHelpers include Common::BaseClass NOT_USED = "no_used!" # Class resolver # @note it caches the resolved `klass`es # @raise [Exception] when could not resolve if `exception` is `true` # @param klass [Class, String, Symbol] the class to resolve # @param source_class [Class] when the reference to `klass` belongs to a different inheritance chain. # @param exception [Boolean] if it should raise exception when could not resolve # @return [Class] the `Class` constant def resolve_class(klass, source_class: self, exception: true) @resolved ||= {} @resolved[klass] ||= case klass when Class klass when String begin Kernel.const_get(klass) rescue NameError => e raise if exception end when Symbol source_class.resolve_class(source_class.send(klass)) when Hash referrer, referred = klass.first resolve_class(referred, source_class: referrer, exception: exception) else raise "Unknown class: #{klass}" if exception end end # Helper to normalize `key` into a correct `ruby` **constant name** # @note it removes namespace syntax `::` # @param key [String, Symbol] to be normalized # @return [String] a correct constant name def to_constant(key) str_name = key.to_s.strip.split(/::/).compact.map do |str| str.slice(0).upcase + str.slice(1..-1) end.join("").split(/[\-\_ :]+/i).compact.map do |str| str.slice(0).upcase + str.slice(1..-1) end.join("") end # Helper to create an instance variable `name` # @param [String, Symbol] the name of the variable # @reutrn [String] the name of the created instance variable def instance_variable_name(name) str = name.to_s str = "@#{str}" unless str.start_with?("@") str end # Generates random ids in hexadecimal to use in class name generation. # @param len [Integeter] length of the `uid` # @return [String] a random unique id of length `len` def uid(len = 8); SecureRandom.hex(len/2) end # If the class for `name` exists, it returns it. Otherwise it generates it. # @param name [String, Symbol] the name of the new class # @param inherits [Class] the parent class to _inherit_ from # @param namespace [Class, String] an existing `constant` (class or module) the new class will be namespaced on # @yield [child_class] configure the new class # @yieldparam child_class [Class] the new class # @return [Class] the new generated class def new_class(name = "Child#{uid}", inherits: self, namespace: inherits) name = name.to_s.to_sym.freeze class_name = to_constant(name) unless target_class = resolve_class("#{namespace}::#{class_name}", exception: false) target_class = Class.new(inherits) Kernel.const_get(namespace.to_s).const_set class_name, target_class end target_class.tap do |klass| yield(klass) if block_given? end end # Helper to parse a value into a `Time` object. # @raise [Exception] if `exception` is `true` and could not convert # @param value [String, Date] the value to convert to `Time` # @param exception [Boolean] if should raise `Exception` when could not convert # @return def to_time(value, exception: true) case value when NilClass value when String begin Time.parse(value) rescue ArgumentArgument => e raise if exception nil end when Date Time.parse(value.to_s) when Time value else to_time(value.to_s) if value.respond_to?(:to_s) end end # Helper to determine if a paramter has been used # @note to effectivelly use this helper, you should initialize your target # paramters with the constant `NOT_USED` # @param val [] the value of the paramter # @return [Boolean] `true` if value other than `NOT_USED`, `false` otherwise def used_param?(val) val != NOT_USED end # Keeps track on class instance variables that should be inherited by child classes. # @note # - subclasses will inherit the value as is at that moment # - any change afterwards will be only on the specific class (in line with class instance variables) # - adapted from https://stackoverflow.com/a/10729812/4352306 # TODO: this separates the logic of the method to the instance var. Think if would be possible to join them somehow. def inheritable_class_vars(*vars) @inheritable_class_vars ||= [:inheritable_class_vars] @inheritable_class_vars += vars end # Builds the attr_reader and attr_writer of `attrs` and registers the associated instance variable as inheritable. def inheritable_attrs(*attrs) attrs.each do |attr| class_eval %( class << self; attr_accessor :#{attr} end ) end inheritable_class_vars(*attrs) end # This callback method is called whenever a subclass of the current class is created. # @note # - values of the instance variables are copied as they are (no dups or clones) # - the above means: avoid methods that change the state of the mutable object on it # - mutating methods would reflect the changes on other classes as well # - therefore, `freeze` will be called on the values that are inherited. def inherited(subclass) inheritable_class_vars.each do |var| instance_var = instance_variable_name(var) value = instance_variable_get(instance_var) subclass.instance_variable_set(instance_var, value.freeze) end end end end end end end