README.md in moosex-0.0.19 vs README.md in moosex-0.0.20

- old
+ new

@@ -3,20 +3,23 @@ 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) [![Code Climate](https://codeclimate.com/github/peczenyj/MooseX.png)](https://codeclimate.com/github/peczenyj/MooseX) [![Dependency Status](https://gemnasium.com/peczenyj/MooseX.png)](https://gemnasium.com/peczenyj/MooseX) [![Coverage Status](https://coveralls.io/repos/peczenyj/MooseX/badge.png)](https://coveralls.io/r/peczenyj/MooseX) [![githalytics.com alpha](https://cruel-carlota.pagodabox.com/f51f40f92298589b598a55bc753977f9 "githalytics.com")](http://githalytics.com/peczenyj/MooseX) ## Introduction This is another DSL for object creation, aspects, method delegation and much more. It is based on Perl Moose and Moo, two important modules who add a better way of Object Orientation development (and I enjoy A LOT). Using a declarative style, using Moose/Moo you can create attributes, methods, the entire constructor and much more. But I can't find something similar in Ruby world, so I decide port a small subset of Moose to create a powerfull DSL for object construction. +Want to help? Install the gem `moosex` and start to use. If you find some issue, please assign to me :) + Of course, there is few similar projects in ruby like - [Virtus](https://github.com/solnic/virtus) - [Active Record Validations](http://edgeguides.rubyonrails.org/active_record_validations.html) But the objetive of MooseX is different: this is a toolbox to create Classes based on DSL, with unique features like -- method delegation ( see 'handles') +- method delegation and currying ( see 'handles') - lazy attributes -- roles +- roles / abstract classes / interfaces +- traits / monads - parameterized roles - composable type check - events and much more. @@ -30,12 +33,14 @@ - [MooseX::Role::Parameterized](http://search.cpan.org/~sartak/MooseX-Role-Parameterized-1.02/lib/MooseX/Role/Parameterized/Tutorial.pod) See also: - [Reindeer](https://github.com/broquaint/reindeer), another Moose port to Ruby (still on 0.0.1 version) - [Joose](https://code.google.com/p/joose-js/), a javascript port of Moose. -- [Perl 6](http://en.wikipedia.org/wiki/Perl_6#Object-oriented_programming) Perl 6 OO programming style. +- [Perl 6](http://en.wikipedia.org/wiki/Perl_6#Object-oriented_programming), Perl 6 OO programming style. +- [Elk](http://frasertweedale.github.io/elk/), Elk is an object system for Python inspired by Moose. + Why MooseX? Because the namespace MooseX/MooX is open to third-party projects/plugins/extensions. You can upgrade your Moo(se) class using other components if you want. And there is one gem called 'moose' :/ THIS MODULE IS EXPERIMENTAL YET! BE CAREFUL! Talk is cheap. Show me the code! @@ -488,13 +493,13 @@ ### override => true|false If you need override one attribute, you should use `override: true`, or MooseX will raise one exception. -### traits => Trait|[Array of Traits] +### traits => Trait | [Array of Traits] -The objective of use traits is extends the original attribute, using delegators. We support few traits at this moment and, **Important**, if you set a list of Traits we will apply each trait in sequence. Have a Suggestion? Open an Issue on Github! +The objective of use traits is extends the original attribute, using delegators (think in Monads). We support few traits at this moment and, **Important**, if you set a list of Traits we will apply each trait in sequence. Have a Suggestion? Open an Issue on Github! #### Trait Counter ```ruby class MyHomePage @@ -552,35 +557,42 @@ } ``` We store surname and name as a tuple of Strings. Instead access `array_with_surname_name[0]` or `array_with_surname_name[1]`, we can apply the Trait `Pair`, and access (or save!) each component of this pair, but you can't change the pair itself. Look the example of currying in `surname_and_name`, calling join with "," as an argument. -#### Trait RescueToNil +#### Trait Expires -The objective of `MooseX::Traits::RescueToNil` is avoid `NoMethodError` if, for example, you set nil as value. For example: +This trait will wrap the original value and set one expiration time (in seconds, -1 to never expires). Accepts one tuple (array) of [ value, expiration ]. ```ruby - has important: { +requires 'moosex' +requires 'moosex/traits' + +class MyHomePage + include MooseX + has session: { is: :rw, - default: 0, - traits: MooseX::Traits::RescueToNil, - handles: { - plus: :+, - minus: :-, - } + default: -> { {} }, + coerce: ->(value) { ((value.is_a?(Array))? value : [value, 3]) }, + traits: MooseX::Traits::Expires, } -``` -Imagine you can accept nil as a valid value. In this case, you can't use `+` or `-`, right? It will raise a NoMethodError. Well... you can avoid this with RescueToNil trait. Using this, we will return `nil` for each operation in case of NoMethodError, and raise other kinds of exceptions. +end -#### Trait RescueToZero +page = MyHomePage.new +page.session.valid? # => true +page.session # => {} +sleep(3) +page.session.valid? # => false +page.session= { bar: 5 } # will be coerce to [ {bar: 5}, 3 ] +page.session.valid? # => true +page.session # => { bar: 5 } +sleep(3) +page.session.valid? # => false +``` -Similar to RescueToNil, but return 0 in case of `NoMethodError`. +See plugin "ExpiredAttribute" for a more clean interface (without this ugly coerce)! -#### Trait RescueToEmptyString - -Similar to RescueToNil, but return empty string "" in case of `NoMethodError`. - ##### Create your own trait You should create a Class with a constructor who will receive a reference to the value. For example, the trait Counter is using SimpleDelegator: ```ruby @@ -601,52 +613,113 @@ end ... ``` You can create or extend your own Traits too. It is easy. -##### Composable Traits +#### Traits Removed -It is easy compose traits, for example: +**IMPORTANT** RescueToNil, RescueToZero and RescueToEmptyString traits are removed since 0.0.20 version +## Plugins + +You can extend MooseX using Plugins, and you can create your own plugins. The only kind of plugin supported is `attribute plugin`, but we can accept other kinds of plugins in the future. To enable one ( or more ) plugins you should specify the list of plugins in the init method when you include the MooseX module. + +### Plugin Chained + +The original behavior of the writter method is return the attribute value. If you want return `self` to continue calling other methods - to create one fluent interface, for example - you can use the plugin `MooseX::Plugins::Chained` and avoid writters with = in the end. For example: + ```ruby -class ComplexExample - include MooseX - include MooseX::Types +require 'moosex' +require 'moosex/plugins' - has surname_name: { - is: :rw, - isa: isMaybe(isTuple(String, String)), - default: nil, - traits: [ MooseX::Traits::RescueToEmptyString, MooseX::Traits::Pair ], - handles: { - surname: :first, - name: :second, - :surname= => :first=, - :name= => :second=, - surname_and_name: { join: ->{", "} } - } +class EmailMessage + include MooseX.init( + with_plugins: MooseX::Plugins::Chained + ) + has :_from, writter: :from, chained: true + has :_to, writter: :to, chained: true + has :_subject, writter: :withSubject, chained: true + has :_body , writter: :withBody, chained: true + + def send! + # add logic + end +end + +EmailMessage.new. + from("foo@bar.com"). + to("me@baz.com"). + withSubject("test"). + withBody("hi!"). + send! +``` + +### Plugin ExpiredAttribute + +It is a easy way to apply the trait Expired in lazy attributes! + +```ruby +require 'moosex' +require 'moosex/plugins' + +class MyClass + include MooseX.init(with_plugins: MooseX::Plugins::ExpiredAttribute) + + has config: { + is: :lazy, + clearer: true, # mandatory + expires: 60, # seconds } + + def build_config + # read configuration... + end + end ``` -First, we apply `RescueToEmptyString`, then `Pair`. In this case, if you set `nil`, name and surname will act as empty string values. For example: +Instead force a coerce to a tuple, here we use a `expires` keyword. You need enable the clearer to read the configuration, in this case. + +### Build your own Plugin + +You should create one Class who accepts one parameter in the constructor (it is a reference for the MooseX::Attribute class) and one method 'process' who will be invoked against the argument hash ( in the constructor ). The reference for the attribute can be used to change the original behavoir and you must delete the used arguments from the hash (in process). See the file `lib/moosex/plugins.rb` for more examples. + ```ruby -ce = ComplexExample.new(surname_name: nil) -ce.name # => "" -ce.surname_and_name # => ", " +module MooseX + module Plugins + class Chained + def initialize(this) + @this = this + end + def process(options) + chained = !! options.delete(:chained) -ce.name= "Isaac" -ce.surname_and_name # => ", Isaac" -ce.surname= "Asimov" -ce.surname_and_name # => "Asimov, Isaac" + if chained + writter = @this.attribute_map[:writter] + old_proc = @this.methods[ writter ] + @this.methods[writter] = ->(this, value) { old_proc.call(this, value); this } + end -ce.surname_name= nil -ce.name # => "" -ce.surname_and_name # => ", " + @this.attribute_map[:chained] = chained + end + end + ... ``` -In this example it is safe set nil to `surname_name`. when we try to create the Pair, the [0] and [1] calls will return "", and we have one pair ["", ""]. This is why we add RescueToEmptyString first. If we use Pair as first trait, Pair expects an array, not a nil. *The Order is Important*. +**Important** the public API for MooseX::Attribute is under development and can change in any moment. This will be true until the first stable release. +### Enable more than one plugin + +You can pass the list of plugins as an array. + +```ruby +require 'moosex' +require 'moosex/plugins' + +class MyClass + include MooseX.init(with_plugins: [ MooseX::Plugins::ExpiredAttribute, MooseX::Plugins::Chained ]) +``` + ## 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 @@ -663,13 +736,11 @@ class Point3D < Point has z: { is: :rw, required: true } - after :clear! do |object| - object.z = 0 - end + after :clear! ->(this) { this.z = 0 } 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. @@ -695,25 +766,25 @@ def my_method(x) # do something end - before :my_method do |this, x| + before :my_method ->(this, x) do puts "#{Time.now} before my_method(#{x})" end - after :my_method do |this, x| + after :my_method ->(this, x) do puts "#{Time.now} after my_method(#{x})" end end ``` ### around (method| ARRAY) => lambda The around hook is agressive: it will substitute the original method for a lambda. This lambda will receive the original method as a lambda, a reference for the object and the argument list, you shuld call the method_lambda using object + arguments ```ruby - around(:sum) do |method_lambda, this, a,b,c| + around(:sum) ->(method_lambda, this, a,b,c) do c = 0 result = method_lambda.call(this,a,b,c) result + 1 end ``` @@ -965,12 +1036,12 @@ ```ruby module EasyCrud include MooseX - on_init do |*attributes| - attributes.each do | attr | + on_init ->(*attributes) do + attributes.each ->(attr) do has attr, { is: :rw, predicate: "has_attr_#{attr}_or_not?" } end end end @@ -987,20 +1058,20 @@ ```ruby module Logabble2 include MooseX - on_init do |args| + on_init ->(args) do args[:klass] = self include Logabble.init(args) end end module Logabble include MooseX - on_init do | args | + on_init ->(args) do klass = args[:klass] || self # <= THIS will guarantee you will methods = args[:methods] || [] # modify the right class methods.each do |method| @@ -1080,15 +1151,15 @@ end end e = Example.new -e.on(:pinged) do |object| +e.on(:pinged) ->(this) do puts "Ping!" end -e.once(:pinged) do |object| +e.once(:pinged) ->(this) do puts "Ping Once!" end e.ping # will print two messages, "Ping!" and "Ping Once!" e.ping # will print just "Ping!" @@ -1097,11 +1168,11 @@ e.ping # will no longer print nothing # you can use arguments # consider you have one logger attribute in this example -listener = e.on(:error) do |obj, message| +listener = e.on(:error) ->(obj, message) do obj.logger.fatal("Error: #{message}") end e.emit(:error, "ops") # will log, as fatal, "Error: ops" @@ -1145,14 +1216,14 @@ } end ep = EventProcessor.new() -ep.on_ping do |obj| +ep.on_ping ->(obj) do puts "receive ping!" end -ep.on_pong do |obj, message| +ep.on_pong ->(obj, message) do puts "receive pong with #{message}!" end ep.ping # will print "receive ping!" ep.pong 1 # will print "receive pong with 1!"