# MooseX A postmodern object DSL for Ruby [![Build Status](https://travis-ci.org/peczenyj/MooseX.png)](https://travis-ci.org/peczenyj/MooseX) [![Gem Version](https://badge.fury.io/rb/moosex.png)](http://badge.fury.io/rb/moosex) THIS MODULE IS EXPERIMENTAL YET! BE CAREFUL! Talk is cheap. Show me the code! ```ruby require 'moosex' class Point include MooseX has x: { is: :rw, # read-write (mandatory) isa: Integer, # should be Integer default: 0, # default value is 0 (constant) } has y: { is: :rw, isa: Integer, default: lambda { 0 }, # you should specify a lambda } def clear! self.x= 0 # to run with type-check you must self.y= 0 # use the setter instad @x= end def to_s "Point[x=#{self.x}, y=#{self.y}]" end end # now you have a generic constructor p1 = Point.new # x and y will be 0 p2 = Point.new( x: 5 ) # y will be 0 p3 = Point.new( x: 5, y: 4) ``` ## Installation Add this line to your application's Gemfile: gem 'moosex' And then execute: $ bundle Or install it yourself as: $ gem install moosex You need ruby 2.0.x or superior. ## Description MooseX is an extension of Ruby object system. The main goal of MooseX is to make Ruby Object Oriented programming easier, more consistent, and less tedious. With MooseX you can think more about what you want to do and less about the mechanics of OOP. It is a port of Moose/Moo from Perl to Ruby world. Read more about Moose on http://moose.iinteractive.com/en/ ## Motivation It is fun ## Usage You just need include the MooseX module in your class and start to describe the attributes with our DSL. This module will inject one smart constructor, acessor and other necessary methods. Instead the normal way of add accessors, constructor, validation, etc ```ruby class Foo attr_accessor :bar, :baz, :bam def initialize(bar=0, baz=0, bam=0) unless [bar, baz, bam].all? {|x| x.is_a? Integer } raise "you should use only Integers to build Foo" end @bar = bar @baz = baz @bam = bam end end ``` you can do this: ```ruby class Foo include MooseX has [:bar, :baz, :bam], { is: :rw, isa: Integer, default: 0 } end ``` ## DSL: the 'has' method The key of the DSL is the 'has' method injected in your class. You should use this method do describe your class and define the behavior like this: ```ruby has :attribute_name, { hash of properties } ``` to describe one new attribute you shoud specify some properties inside a Hash. The only mandatory property is the ':is', to specify how we should create the acessors (if public or private). The options for "has" are as follows: ### is **Required**, may be :ro, :rw, :rwp, :private or :lazy. "ro" specify a read-only attribute - generate only the reader method - you should specify the value in the constructor or using "default". "rw" specify a read-write attribute - generate both reader and writter methods. "rwp" specify a read-write private attribute. Similar to "rw" but the writter is a private method. "private" will generate both reader and writter as private methods "lazy" similar to "ro", but also sets "lazy" to true and "builder" to "build_#{attribute_name}". ### isa You can specify an optional type check for the attribute. Accepts a lambda, and it must raise one exception if the type check fails. If you provides a Class or Module, we will call the 'is_a?' method in the new value againt the Class/Module. We call the type check routine on the constructor and in each call of the writter method. You can specify your own kind of type validation. ```ruby isa: lambda do |new_value| unless new_value.respond_to? :to_sym raise "bar should respond to to_sym method!" end end, ``` Important: if you access the attribute instance name using @attribute_name= you loose the type check feature. You need always set/get the attribute value using the acessors generated by MooseX. ### default You can specify an optional default value to one attribute. If we don't specify in the constructor, we will initialize the attribute with this value. You also can specify one lambda to force object creation. ```ruby default: 0, ``` or ```ruby default: lambda{ MyObject.new }, ``` ### required if true, the constructor will raise error if this attribute was not present. ```ruby required: true, ``` if this attribute has a default value, we will initialize with this value and no exception will be raised. Optional. ### coerce You can try to coerce the attribute value by a lambda before the type check phase. For example you can do ```ruby coerce: lambda{ |new_value| new_value.to_i }, ``` to force a convertion to integer. Or flatten one array, convert to symbol, etc. Optional. ### handles One of the greatest features in MooseX: you can inject methods and delegate the method calling to the attribute. For example, instead do this: ```ruby def some_method(a,b,c) @attribute.some_method(a,b,c) end ``` you simply specify one or more methods to inject. ```ruby handles: [:some_method], ``` If you specify one Module or Class, we will handle all public instance methods defined in that Module or Class ( if Class, we will consider all methods except the methods declared in the superclass). The only limitation is BasicObject (forbidden). If you need rename the method, you can specify a Hash: ```ruby handles: { my_method_1: :method1, my_method_2: :method2, }, ``` Optional. ### trigger You can specify one lambda or method name to be executed in each writter ( if coerce and type check does not raise any exception ). The trigger will be called in each setter and in the constructor if we do not use the default value. Useful to add a logging operation or some complex validation. ```ruby trigger: lambda do |object, new_value| object.logger.log "change the attribute value to #{new_value}" end ``` or ```ruby has a: { is: :rw } has b: { is: :rw, trigger: :verify_if_a_and_b_are_different, } ... def verify_if_a_and_b_are_different(new_value_of_b) if self.a.eql? new_value_of_b raise "a and b should be different!" end end ``` Optional. ### writter You can specify the name of the attribute acessor, default is "#{attribute_name}=". ### reader You can specify the name of the attribute acessor, default is "attribute_name". ### predicate Creates a method who returns a boolean value if the attribute is defined. If true, will create one public "has_#{attribute_name}?" method by default. For example ```ruby class Foo include MooseX has x: { is: :rw, predicate: true, } end foo = Foo.new foo.has_x? # returns false foo.x= 10 foo.has_x? # returns true ``` Important: nil is different than undefined. If you do not initialize one attribute, you will receive one 'nil' if you try to fetch the value, but the state of this attribute is 'undefined'. If you set any value (even nil), the attribute will be considered 'defined' and the predicate will return true. Optional. ### clearer Creates a method who will unset the attribute. If true, will create one public "clear_#{attribute_name}!" method by default. Unset in this case is not 'nil', we will remove the instance variable. For example: ```ruby class Foo include MooseX has x: { is: :rw, predicate: true, clearer: true, } end foo = Foo.new foo.has_x? # returns false foo.x= 10 foo.has_x? # returns true foo.clear_x! # will unset the attribute x foo.has_x? # returns false ``` Optional. ### init_arg You can rename the attribute name in the constructor. For example: ```ruby class Foo include MooseX has secret: { is: :rw, writter: :x=, reader: :x, init_arg: :x, } end foo = Foo.new(x: 1) # specify the value of secret in the constructor foo.x # return 1 foo.x= 2 # will set 'secret' to 2 ``` ### lazy Another great feature: lazy attributes. If you this to true, we will wait until the first reader accessor be called to create the object using the builder method, then store the value. For example: ```ruby class Foo include MooseX has x: { is: :rw, lazy: :true, predicate: true, # predicate and clearer are just clearer: true, # to show a better example } def builder_x Some::Class.new end end foo = Foo.new # normal... foo.has_x? # returns false foo.x # will call the builder_x method and store the value foo.has_x? # returns true foo.x # returns the stored value foo.clear_x! # will unset the attribute x foo.has_x? # returns false foo.x # will call the builder again and store the value ``` A lazy attribute needs a builder method or lambda. By default you should implement the "builder_#{attribute_name}" method. Using lazy you should initialize one attribute when you really need. Optional. ### builder You can specify the builder name if the attribute is lazy, or you can specity one lambda. If true, the default name of the builder will be "builder_#{attribute_name}". This attribute will be ignored if the attribute is not lazy. ```ruby class Foo include MooseX has x: { is: :rw, lazy: :true, builder: lambda{ |foo| Some::Class.new } # you can ignore foo, or use it! } end ``` Optional. ## Hooks: after/before/around Another great feature imported from Moose are the hooks after/before/around one method. You can run an arbitrary code, for example: ```ruby class Point include MooseX has [:x, :y ], { is: :rw, required: true } def clear! self.x = 0 self.y = 0 end end class Point3D < Point has z: { is: :rw, required: true } after :clear! do |object| object.z = 0 end end ``` instead redefine the 'clear!' method in the subclass, we just add a piece of code, a lambda, and it will be executed after the normal 'clear!' method. ### after The after hook should receive the name of the method as a Symbol and a lambda. This lambda will, in the argument list, one reference for the object (self) and the rest of the arguments. This will redefine the the original method, add the code to run after the method. The after does not affect the return value of the original method, if you need this, use the 'around' hook. ### before The before hook should receive the name of the method as a Symbol and a lambda. This lambda will, in the argument list, one reference for the object (self) and the rest of the arguments. This will redefine the the original method, add the code to run before the method. A good example should be logging: ```ruby class Point include MooseX def my_method(x) # do something end before :my_method do |object, x| puts "#{Time.now} before my_method(#{x})" end after :my_method do |object, x| puts "#{Time.now} after my_method(#{x})" end end ``` ### around The around hook is agressive: it will substitute the original method for a lambda. This lambda will receive the original method, a reference for the object and the argument list ```ruby around(:sum) do |original_method, object, a,b,c| result = original_method.bind(object).call(a,b,c) result + 1 end ``` it is useful to manipulate the return value if you need. ## Types MooseX has a built-in type system to be helpful in many circunstances. How many times you need check if some argument is_a? Something? Or it respond_to? :some_method ? Now it is over. If you include the MooseX::Types module in your MooseX class you can use: ### isAny will accept any type. Useful to combine with other types. ```ruby has x: { is: :rw, isa: isAny } ``` ### isConstant will verify using :=== if the value is equal to some contant ```ruby has x: { is: :rw, isa: isConstant(1) } ``` ### isType, isInstanceOf and isConsumerOf will verify the type using is_a? method. should receive a Class. isInstanceOf is an alias, to be used with Classes and isConsumerOf is for Modules. ```ruby has x: { is: :rw, isa: isConsumerOf(Enumerable) } ``` ### hasMethods will verify if the value respond_to? for one or more methods. ```ruby has x: { is: :rw, isa: hasMethods(:to_s) } ``` ### isEnum verify if the value is part of one enumeration of constants. ```ruby has x: { is: :rw, isa: isEnum(:black, :white, :red, :green, :yellow) } ``` ### isMaybe(type) verify if the value isa type or is nil. You can combine with other types. ```ruby has x: { is: :rw, isa: isMaybe(Integer) } # accepts 1,2,3... or nil ``` ### isNot(type) will revert the type check. Useful to combine with other types ```ruby has x: { is: :rw, # x will accept any values EXCEPT :black, :white... isa: isNot(isEnum(:black, :white, :red, :green, :yellow)) } ``` ### isArray(type) Will verify if the value is an Array. Can receive one extra type, and we will verify each element inside the array againt this type, or Any if we not specify the type. ```ruby has x: { is: :rw, isa: isArray() # will accept any array } has y: { is: :rw, # this is a more complex type isa: isArray(isArray(isMaybe(Integer))) } ``` ### isHash(type=>type) similar to isArray. if you do not specify a pair of types, it will check only if the value is_a? Hash. Otherwise we will verify each pair key/value. ```ruby has x: { is: :rw, isa: isHash() # will accept any Hash } has y: { is: :rw, # this is a more complex type isa: isHash(Integer => isArray(isMaybe(Integer))) } ``` ## isSet(type) similar to isSet. the difference is: it will raise one exception if there are non unique elements in this array. ```ruby has x: { is: :rw, isa: isSet(Integer) # will accept [1,2,3] but not [1,1,1] } ``` ## isTuple(types) similar to isArray, Tuples are Arrays with fixed size. We will verify the type of each element. For example, to specify one tuple with three elements, the first is Integer, the second is a Symbol and the las should be a String or nil: ```ruby has x: { is: :rw, isa: isTuple(Integer, Symbol, isMaybe(String)) } ``` ### isAllOf(types) will combine all types and will fail if one of the condition fails. ```ruby has x: { is: :rw, isa: isAllOf( hasMethods(:foo, :bar), # x should has foo and bar methods isConsumerOf(Enumerable), # AND should be an Enumerable isNot(SomeForbiddenClass), # AND and should not be an SomeForbiddenClass ) } ``` ### isAnyOf(types) will combine all types and will fail if all of the condition fails. ```ruby has x: { is: :rw, isa: isAnyOf( hasMethods(:foo, :bar), # x should has foo and bar methods isEnum(1,2,3,4), # OR be 1,2,3 or 4 isHash(isAny => Integer) # OR be an Hash of any type => Integers ) } ``` ## BUILD If you need run some code after the creation of the object (like some extra validation), you should override the BUILD method. ```ruby class BuildExample include MooseX has [:x, :y], { is: :rw, required: true, } def BUILD if self.x == self.y raise "invalid: you should use x != y" end end end b1 = BuildExample.new(x: 1, y: 2) # will create the object b2 = BuildExample.new(x: 1, y: 1) # will raise the exception! ``` ### BUILDARGS If you need manupulate the constructor argument list, you should implement the method BUILDARGS. You MUST return one Hash of attribute_name => value. ```ruby class BuildArgsExample2 include MooseX has [:x, :y], { is: :rw, required: true, } def BUILDARGS(x=4,y=8) args = {} args[:x] = x args[:y] = y args end end ex1 = BuildArgsExample2.new(1,2) # x == 1, y == 2 ex2 = BuildArgsExample2.new(1) # x == 1, y == 8 ex3 = BuildArgsExample2.new() # x == 4, y == 8 ``` ## IMPORTANT This module is experimental. I should test more and more to be possible consider this "production ready". If you find some issue/bug please add here: https://github.com/peczenyj/MooseX/issues Until the first 0.1 version I can change anything without warning. I am open to suggestions too. ## TODO 1. Support to Roles ( it is a Module on Steroids ) 2. Support to after/before/around 3. Improve the typecheck system (we should specify: we need an array of positive integers) 4. Improve the exception and warning system 5. Profit! ## Limitations Experimental module, be careful. Now has limited support to subclassing. ## Contributing 1. Fork it ( http://github.com/peczenyj/MooseX/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request