lib/moosex.rb in moosex-0.0.17 vs lib/moosex.rb in moosex-0.0.18

- old
+ new

@@ -2,675 +2,108 @@ # A postmodern object DSL for Ruby # # Author:: Tiago Peczenyj (mailto:tiago.peczenyj@gmail.com) # Copyright:: Copyright (c) 2014 Tiago Peczenyj # License:: MIT -# + require "moosex/version" require "moosex/types" +require "moosex/exceptions" +require "moosex/meta" +require "moosex/core" +require "moosex/attribute" require "weakref" module MooseX + @@ALIAS = nil @@MOOSEX_WARNINGS = true @@MOOSEX_FATAL = false - - class FatalError < StandardError - end - + def self.warn(x, *c) - raise FatalError, "[MooseX] exception: #{x}",*c if @@MOOSEX_FATAL - Kernel.warn("[MooseX] warning: #{x}") if @@MOOSEX_WARNINGS + if @@MOOSEX_FATAL + raise FatalError, "[MooseX] exception: #{x}",*c + elsif @@MOOSEX_WARNINGS + Kernel.warn("[MooseX] warning: #{x}") + end end def self.init(args={}) if args.has_key? :warnings @@MOOSEX_WARNINGS = !! args[:warnings] end if args.has_key? :fatal @@MOOSEX_FATAL = !! args[:fatal] end + + if args.has_key?(:meta) && !! args[:meta] + @@ALIAS = (args[:meta].is_a?(TrueClass))? :meta : args[:meta] + end self end - class RequiredMethodNotFoundError < NameError - end + def initialize(*args) + if self.respond_to? :BUILDARGS + args = self.BUILDARGS(*args) + else + args = args[0] + end - def MooseX.included(c) - - c.extend(MooseX::Core) + self.class.__moosex__meta.init(self, args || {}) - def c.init(*args) - __meta.roles.each{|role| role.call(*args)} - - self - end - - def c.included(x) - - MooseX.included(x) - x.__meta.load_from(self.__meta) - - return unless x.is_a? Class - - x.__meta.load_hooks(self.__meta) - self.__meta.init_klass(x) - - x.__meta.requires.each do |method| - unless x.public_instance_methods.include? method - MooseX.warn "you must implement method '#{method}' in #{x} #{x.class}: required"# if $MOOSEX_DEBUG - end - end - end - - meta = MooseX::Meta.new - - unless c.respond_to? :__meta - c.class_exec do - define_singleton_method(:__meta) { meta } - define_singleton_method(:__meta_define_method) do |method_name, &proc| - define_method(method_name, proc) - end - end - end - - def initialize(*args) - if self.respond_to? :BUILDARGS - args = self.BUILDARGS(*args) - else - args = args[0] - end - - self.class.__meta().init(self, args || {}) - - self.BUILD() if self.respond_to? :BUILD - end - - def c.inherited(subclass) - subclass.class_exec do - old_meta = subclass.__meta - - meta = MooseX::Meta.new(old_meta) - - define_singleton_method(:__meta) { meta } - end - end - + self.BUILD() if self.respond_to? :BUILD end - - class Meta - attr_reader :attrs, :requires, :before, :after, :around, :roles - def initialize(old_meta=nil) - @initialized = false - @attrs = {} - @requires = [] - @roles = [] - @before = Hash.new { |hash, key| hash[key] = [] } - @after = Hash.new { |hash, key| hash[key] = [] } - @around = Hash.new { |hash, key| hash[key] = [] } + def MooseX.included(class_or_module) - if old_meta - old_meta.attrs.each_pair do |key, value| - @attrs[key] = value.clone - end - @requires = old_meta.requires.clone - end - end + class_or_module.extend(MooseX::Core) - def load_from(other_meta) - other_meta.attrs.each_pair do |key, value| - @attrs[key] = value.clone - end - @requires += other_meta.requires - end - - def load_hooks(other_meta) - other_meta.before.each_pair do |m, b| - @before[m] += b.clone - end - other_meta.after.each_pair do |m, b| - @after[m] += b.clone - end - other_meta.around.each_pair do |m, b| - @around[m] += b.clone - end - end + unless class_or_module.respond_to? :__moosex__meta + meta = MooseX::Meta.new - def add(attr) - if @attrs.has_key?(attr.attr_symbol) && ! attr.override - raise FatalError, "#{attr.attr_symbol} already exists, you should specify override: true" - end - @attrs[attr.attr_symbol] = attr - end + class_or_module.define_singleton_method(:__moosex__meta) { meta } - def add_requires(method) - @requires << method - end - - def add_before(method_name, block) - @before[method_name] << block.clone - end - - def add_after(method_name, block) - @after[method_name] << block.clone - end - - def add_around(method_name, block) - @around[method_name] << block.clone - end - - def add_role(block) - @roles << block - end - - def init_klass(klass) - #return if @initialized - - [@before.keys + @after.keys + @around.keys].flatten.uniq.each do |method_name| - begin - method = klass.instance_method method_name - rescue => e - MooseX.warn "Unable to apply hooks (after/before/around) in #{klass}::#{method_name} : #{e}" # if $MOOSEX_DEBUG - next + if @@ALIAS + class_or_module.class_eval do + class_or_module.define_singleton_method(@@ALIAS) { meta } end - - before = @before[method_name] - after = @after[method_name] - around = @around[method_name] - - klass.__meta_define_method(method_name) do |*args, &proc| - before.each{|b| b.call(self,*args, &proc)} - - original = lambda do |object, *args, &proc| - method.bind(object).call(*args, &proc) - end - - result = around.inject(original) do |lambda1, lambda2| - lambda2.curry[lambda1] - end.call(self, *args, &proc) - - after.each{|b| b.call(self,*args, &proc)} - - result - end - end + @@ALIAS = false + end end - - def init(object, args) - @attrs.each_pair{ |symbol, attr| attr.init(object, args) } - - MooseX.warn "unused attributes #{args} for #{object.class}", caller unless args.empty? - - @requires.each do |method| - unless object.respond_to? method - raise RequiredMethodNotFoundError, - "you must implement method '#{method}' in #{object.class}: required" - end - end - end - end - - module Core - def on_init(&block) - __meta.add_role(block) - end - def after(*methods_name, &block) - methods_name.each do |method_name| - begin - method = instance_method method_name - - define_method method_name do |*args, &proc| - result = method.bind(self).call(*args, &proc) - block.call(self,*args,&proc) - result - end - rescue => e - MooseX.warn "unable to apply hook after in #{method_name} @ #{self}: #{e}", caller() if self.is_a?(Class) - __meta.add_after(method_name, block) - end - end + def class_or_module.init(*args) + __moosex__meta.init_roles(*args) + + self end - def before(*methods_name, &block) - methods_name.each do |method_name| - begin - method = instance_method method_name + def class_or_module.inherited(subclass) + old_meta = subclass.__moosex__meta - define_method method_name do |*args, &proc| - block.call(self,*args, &proc) - method.bind(self).call(*args, &proc) - end - rescue => e - MooseX.warn "unable to apply hook before in #{method_name} @ #{self}: #{e}", caller() if self.is_a?(Class) - __meta.add_before(method_name, block) - end - end - end + meta = MooseX::Meta.new(old_meta) - def around(*methods_name, &block) - methods_name.each do |method_name| - begin - - method = instance_method method_name - - code = Proc.new do | o, *a, &proc| - method.bind(o).call(*a,&proc) - end - - define_method method_name do |*args, &proc| - block.call(code, self,*args, &proc) - end - - rescue => e - MooseX.warn "unable to apply hook around in #{method_name} @ #{self}: #{e}", caller() if self.is_a?(Class) - __meta.add_around(method_name, block) - end - end - end - - def requires(*methods) - - methods.each do |method_name| - __meta.add_requires(method_name) - end - end - - def has(attr_name, attr_options = {}) - if attr_name.is_a? Array - attr_name.each do |attr| - has(attr, attr_options) - end - elsif attr_name.is_a? Hash - attr_name.each_pair do |attr, options | - has(attr, options) - end - else - attr = MooseX::Attribute.new(attr_name, attr_options, self) - - __meta.add(attr) - - attr.methods.each_pair do |method, proc| - define_method method, &proc - end - - if attr.is.eql?(:rwp) - private attr.writter - elsif attr.is.eql?(:private) - private attr.writter - private attr.reader - end - end - end - end - - class InvalidAttributeError < TypeError - - end - - class Attribute - include MooseX::Types - - attr_reader :attr_symbol, :is, :reader, :writter, :lazy, :builder, :methods, :override - DEFAULTS= { - is: :rw, - weak: false, - lazy: false, - clearer: false, - required: false, - predicate: false, - isa: isAny, - handles: {}, - trigger: lambda {|object,value|}, # TODO: implement - coerce: lambda {|object| object}, # TODO: implement - doc: nil, - override: false, - } - - REQUIRED = [] - - VALIDATE = { - is: lambda do |is, field_name| - unless [:rw, :rwp, :ro, :lazy, :private].include?(is) - raise InvalidAttributeError, "invalid value for field '#{field_name}' is '#{is}', must be one of :private, :rw, :rwp, :ro or :lazy" - end - end, - }; - - COERCE = { - is: lambda do |is, field_name| - is.to_sym - end, - isa: lambda do |isa, field_name| - isType(isa) - end, - default: lambda do |default, field_name| - return default if default.is_a? Proc - - return lambda { default } - end, - required: lambda do |required, field_name| - !!required - end, - lazy: lambda do |lazy, field_name| - !!lazy - end, - predicate: lambda do |predicate, field_name| - if ! predicate - return false - elsif predicate.is_a? TrueClass - return "has_#{field_name}?".to_sym - end - - begin - predicate.to_sym - rescue => e - # create a nested exception here - raise InvalidAttributeError, "cannot coerce field predicate to a symbol for #{field_name}: #{e}" - end - end, - clearer: lambda do|clearer, field_name| - if ! clearer - return false - elsif clearer.is_a? TrueClass - return "clear_#{field_name}!".to_sym - end + subclass.define_singleton_method(:__moosex__meta) { meta } + end - begin - clearer.to_sym - rescue => e - # create a nested exception here - raise InvalidAttributeError, "cannot coerce field clearer to a symbol for #{field_name}: #{e}" - end - end, - handles: lambda do |handles, field_name| - - unless handles.is_a? Hash - - array_of_handles = handles - - unless array_of_handles.is_a? Array - array_of_handles = [ array_of_handles ] - end - - handles = array_of_handles.map do |handle| - - if handle == BasicObject - - raise InvalidAttributeError, "ops, should not use BasicObject for handles in #{field_name}" - - elsif handle.is_a? Class - - handle = handle.public_instance_methods - handle.superclass.public_instance_methods - - elsif handle.is_a? Module - - handle = handle.public_instance_methods - - end - - handle - - end.flatten.reduce({}) do |hash, method_name| - hash.merge({ method_name => method_name }) - end - end - - handles.map do |key,value| - if value.is_a? Hash - raise "ops! Handle should accept only one map / currying" unless value.count == 1 - - original, currying = value.shift - - { key.to_sym => [original.to_sym, currying] } - else - { key.to_sym => value.to_sym } - end - end.reduce({}) do |hash,e| - hash.merge(e) - end - end, - reader: lambda do |reader, field_name| - reader.to_sym - end, - writter: lambda do |writter, field_name| - writter.to_sym - end, - builder: lambda do |builder, field_name| - unless builder.is_a? Proc - builder_method_name = builder.to_sym - builder = lambda do |object| - object.send(builder_method_name) - end - end - - builder - end, - init_arg: lambda do |init_arg, field_name| - init_arg.to_sym - end, - trigger: lambda do |trigger, field_name| - unless trigger.is_a? Proc - trigger_method_name = trigger.to_sym - trigger = lambda do |object, value| - object.send(trigger_method_name,value) - end - end - - trigger - end, - coerce: lambda do |coerce, field_name| - unless coerce.is_a? Proc - coerce_method_name = coerce.to_sym - coerce = lambda do |object| - object.send(coerce_method_name) - end - end - - coerce - end, - weak: lambda do |weak, field_name| - !! weak - end, - doc: lambda do |doc, field_name| - doc.to_s - end, - override: lambda do |override, field_name| - !! override - end, - }; - - def initialize(a, o ,x) - #o ||= {} - # todo extract this to a framework, see issue #21 on facebook - o = DEFAULTS.merge({ - reader: a, - writter: a.to_s.concat("=").to_sym, - builder: "build_#{a}".to_sym, - init_arg: a, - }).merge(o) - - REQUIRED.each { |field| - unless o.has_key?(field) - raise InvalidAttributeError, "field #{field} is required for Attribute #{a}" - end - } - COERCE.each_pair do |field, coerce| - if o.has_key? field - o[field] = coerce.call(o[field], a) - end - end - VALIDATE.each_pair do |field, validate| - return if ! o.has_key? field - - validate.call(o[field], a) - end - - if o[:is].eql? :ro - o[:writter] = nil - elsif o[:is].eql? :lazy - o[:lazy] = true - o[:writter] = nil - end - - unless o[:lazy] - o[:builder] = nil - end - - if o[:weak] - old_coerce = o[:coerce] - o[:coerce] = lambda do |value| - WeakRef.new old_coerce.call(value) - end - end - - @attr_symbol = a - @is = o.delete(:is) - @isa = o.delete(:isa) - @default = o.delete(:default) - @required = o.delete(:required) - @predicate = o.delete(:predicate) - @clearer = o.delete(:clearer) - @handles = o.delete(:handles) - @lazy = o.delete(:lazy) - @reader = o.delete(:reader) - @writter = o.delete(:writter) - @builder = o.delete(:builder) - @init_arg = o.delete(:init_arg) - @trigger = o.delete(:trigger) - @coerce = o.delete(:coerce) - @weak = o.delete(:weak) - @documentation = o.delete(:doc) - @override = o.delete(:override) - @methods = {} - - MooseX.warn "Unused attributes #{o} for attribute #{a} @ #{x} #{x.class}",caller() if ! o.empty? - - if @reader - @methods[@reader] = generate_reader - end + def class_or_module.included(other_class_or_module) - if @writter - @methods[@writter] = generate_writter - end - inst_variable_name = "@#{@attr_symbol}".to_sym - if @predicate - @methods[@predicate] = Proc.new do - instance_variable_defined? inst_variable_name - end - end + MooseX.included(other_class_or_module) - if @clearer - @methods[@clearer] = Proc.new do - if instance_variable_defined? inst_variable_name - remove_instance_variable inst_variable_name - end - end - end - - attr_symbol = @attr_symbol - @handles.each_pair do | method, target_method | - if target_method.is_a? Array - original, currying = target_method + other_class_or_module.__moosex__meta.load_from(self) - @methods[method] = Proc.new do |*args, &proc| - - a1 = [ currying ] - - if currying.is_a?Proc - a1 = currying.call() - elsif currying.is_a? Array - a1 = currying.map{|c| (c.is_a?(Proc)) ? c.call : c } - end - - self.send(attr_symbol).send(original, *a1, *args, &proc) - end - else - @methods[method] = Proc.new do |*args, &proc| - self.send(attr_symbol).send(target_method, *args, &proc) - end - end - end - end - - def init(object, args) - value = nil - value_from_default = false - - if args.has_key? @init_arg - value = args.delete(@init_arg) - elsif @default - value = @default.call - value_from_default = true - elsif @required - raise InvalidAttributeError, "attr \"#{@attr_symbol}\" is required" - else - return - end + if other_class_or_module.is_a? Class - value = @coerce.call(value) - begin - @isa.call( value ) - rescue MooseX::Types::TypeCheckError => e - raise MooseX::Types::TypeCheckError, "isa check for field #{attr_symbol}: #{e}" - end - unless value_from_default - @trigger.call(object, value) - end - inst_variable_name = "@#{@attr_symbol}".to_sym - object.instance_variable_set inst_variable_name, value - end + other_class_or_module.__moosex__meta.load_from_klass(self) - private - def generate_reader - inst_variable_name = "@#{@attr_symbol}".to_sym - - builder = @builder - before_get = lambda {|object| } + self.__moosex__meta.init_klass(other_class_or_module).each_pair do |method_name, proc| + other_class_or_module.class_eval do + define_method(method_name,&proc) + end + end - if @lazy - type_check = @isa - coerce = @coerce - trigger = @trigger - before_get = lambda do |object| - return if object.instance_variable_defined? inst_variable_name + other_class_or_module.__moosex__meta.verify_requires_for(other_class_or_module) - value = builder.call(object) - value = coerce.call(value) - begin - type_check.call( value ) - rescue MooseX::Types::TypeCheckError => e - raise MooseX::Types::TypeCheckError, "isa check for #{inst_variable_name} from builder: #{e}" - end - - trigger.call(object, value) - object.instance_variable_set(inst_variable_name, value) - end - end - - Proc.new do - before_get.call(self) - instance_variable_get inst_variable_name - end + end end - - def generate_writter - writter_name = @writter - inst_variable_name = "@#{@attr_symbol}".to_sym - coerce = @coerce - type_check = @isa - trigger = @trigger - Proc.new do |value| - value = coerce.call(value) - begin - type_check.call( value ) - rescue MooseX::Types::TypeCheckError => e - raise MooseX::Types::TypeCheckError, "isa check for #{writter_name}: #{e}" - end - trigger.call(self,value) - instance_variable_set inst_variable_name, value - end - end end end