# frozen_string_literal: true require 'logger' module Crate class Crate # rubocop:disable Metrics/ClassLength class << self def _initialize(**options) @_names = [] @_crates = {} @_arrays = {} @_hooks = [] @_options = { logger: Logger.new(nil), **options } @_crate_klasses = [] end def inherited(subclass) subclass._initialize(**_options) end def _class(**options, &initializer) Class.new(self) do _initialize(**options) class_eval(&initializer) if block_given? end end def crate(*args, **options, &initializer) name, klass, = args declare(name) @_crates[name] = klass || _class( **_options, basename: name.to_s, **options, &initializer ) end def array(*args, **options, &initializer) name, klass, = args declare(name) @_arrays[name] = klass || _class( **_options, basename: name.to_s, **options, &initializer ) end def versions(*crate_klasses) @_crate_klasses = crate_klasses end def version(*args_, &block) key, value, = *args_ block ||= proc { |raw| raw[key] == value } (class << self; self end) \ .send(:define_method, :_valid_crate?) do |*args| block.call(*args) end end FILTERS = %i[default convert type strip required must_be].freeze def _invalid_filter?(**options) !(options.keys - FILTERS).empty? end def declare(name, **options) raise if _invalid_filter?(**options) return if self == ::Crate::Crate || \ name.nil? || method_defined?(name) _declare_impl(name) _make_hooks(name, **options) end def _declare_impl(name) _names.push(name) define_method(name) { _raw[name] } end def _make_args(hook_name, args) # rubocop:disable Metrics/MethodLength args = [] if args == true args = Array(args) args, block = if args[-1].respond_to?(:to_proc) [args[0...-1], args[-1]] else [args, nil] end args.push({}) unless args[-1].is_a?(Hash) args[-1] = { tag: hook_name, **args[-1] } [args, block] end def _make_hooks(name, **options) options.each do |hook_name, args| args, block = _make_args(hook_name, args) hook { method(:"_hook_#{hook_name}").call(name, *args, &block) } end end def hook(&hook_) _hooks.push(hook_) end def check_undefined_keys(**options) hook do method(:_hook_check_undefined_keys) \ .call(tag: 'check_undefined_keys', **options) end end attr_reader :_options, :_names, :_crates, :_arrays, :_hooks, :_crate_klasses def _basename _options[:basename] end end _initialize def _log(**options) options[:logger].add(options[:severity]) do "#{options[:name]}: [#{options[:tag]}]" + \ (block_given? ? " #{yield}" : '') end end def _fullname _options[:fullname] end def _name(name) "#{_fullname}[#{name.inspect}]" end def _array_name(name, index) "#{_name(name)}[#{index}]" end attr_reader :_raw, :_options def initialize(raw, **options) @_options = { **self.class._options, fullname: self.class._basename, **options } @_raw = Raw.new(raw, **_options).to_hash _new_crates _new_arrays _run_hooks _select_klass_if_exist(**options) end def _new_crates self.class._crates \ .select { |name, _klass| _raw.key?(name) } \ .each do |name, klass| _raw[name] = klass.new(_raw[name], **_options, fullname: _name(name)) end end def _new_arrays self.class._arrays \ .select { |name, _klass| _raw.key?(name) } \ .each do |name, klass| _raw[name].map!.with_index do |raw, index| klass.new(raw, **_options, fullname: _array_name(name, index)) end end end def _run_hooks self.class._hooks.each { |hook| instance_eval(&hook) } end def _select_klass_if_exist(**options) return if self.class._crate_klasses.empty? _select_klass(_find_valid_klass(_raw, **options)) end def _find_valid_klass(*args) self.class._crate_klasses.find do |klass| klass._valid_crate?(*args) end end REJECT_METHODS = %i[object_id __send__ instance_eval].freeze def _exit_if_no_klass(klass) return unless klass.nil? _log(**_options, name: _fullname, tag: 'version', severity: Logger::FATAL) do 'must find valid version but did not' end exit 1 end def _select_klass(klass) _exit_if_no_klass(klass) @_crate = klass.new(_raw, **_options) (methods - REJECT_METHODS).each do |method_name| instance_eval("undef #{method_name.inspect}") end (class << self; self end) \ .send(:define_method, :method_missing) do |*args, &block| @_crate.send(*args, &block) end end def _crate_serialize Hash[_raw.map { |key, value| [key, value._crate_serialize] }] end def to_json _crate_serialize.to_json end def to_pretty_json JSON.pretty_generate(_crate_serialize) end include Hook end end class Object alias _crate_serialize itself end class Array def _crate_serialize map(&:_crate_serialize) end end