mallow 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Mallow #
2
+
3
+ Mallow is an engine and DSL for pattern matching and transforming Ruby objects that mildly eases the task of processing heterogeneous data sets. It is small, stateless, and strives to take simultaneous advantage of neat-o Ruby language features and functional programming techniques, while also reinventing ~~as few wheels as possible~~ ~~relatively few wheels~~ < 1 wheel / 20 LOC.
4
+
5
+ An example of Mallow's versatility is Graham, a tiny testing library powered by Mallow and used for Mallow's own unit tests.
6
+
7
+ ## Papa teach me to mallow ##
8
+
9
+ To mallow is very simple little boy: first marshal, then mallow!
10
+
11
+ ```ruby
12
+ mallow = Mallow::Core.build do |match|
13
+ match.a_hash.to {"#{keys.first} #{values.first}"}
14
+ end
15
+ ```
16
+ Now feed your mallow some iterable data:
17
+ ```ruby
18
+ data = [{:hay => :good_buddy}]
19
+ mallow.fluff data #=> ["hay good_buddy"]
20
+ ```
21
+ Mallow's DSL has a moderately rich vocabulary of built-in helpers (with complementary method_missing magic if that's yr thing):
22
+ ```ruby
23
+ Mallow.fluff { |match|
24
+ match.a(Float).to &:to_i
25
+ match.tuple(3).where{last != 0}.to {|a,b,c| (a + b) / c}
26
+ match.an(Array).and_hashify_with( :name, :age ).and_make_a( Person ).and &:save!
27
+ match.a_fixnum.of_size(8).to {'8bit'}
28
+ match.a_string.to_upcase
29
+ match.*.to { WILDCARD }
30
+ }.fluff( data )
31
+ ```
32
+
33
+ ### Metadata ###
34
+
35
+ A mallow is stateless, so it can't supply internal metadata (like index or match statistics) to rules. But that is not necessary for two reasons. First:
36
+ ```ruby
37
+ Mallow.fluff do |match|
38
+ line = 0
39
+ match.a(Fixnum).to {"Found a fixnum on line #{line+=1}"}
40
+ match.*.to {|e| line+=1;e}
41
+ end
42
+ ```
43
+ But that is just awful, and will betray you if you forget to increment the line number or define your rules in different lexical environments.
44
+
45
+ Luckily the second reason is that this should be done as part of some kind of post-processing anyway. To aid in such an undertaking, Mallow wraps a matched element in its _own_ metadata, which can be accessed transparently at any point in the transformer chain once a match has succeeded:
46
+ ```ruby
47
+ doubler = Mallow.fluff do |m|
48
+ m.a(Fixnum).with_metadata(type: Fixnum).to {|n| n*2}
49
+ m.anything.to {nil}.^(matched: false) # alias
50
+ end
51
+
52
+ data = doubler.fluff [1,2,:moo] #=> [2, 4, nil]
53
+ metadata = doubler._fluff [1,2,:moo] #=> [{:type=>Fixnum}, {:type=>Fixnum}, {:matched=>false}]
54
+ metadata.map(&:val) #=> [2, 4, nil]
55
+ ```
56
+
57
+ ### Blocks & bindings ###
58
+
59
+ When a matcher is passed a parameter-less block, Mallow evaluates that block in the context of the element running against the matcher:
60
+ ```ruby
61
+ Mallow.fluff {|m| m.*.to {self} }.fluff1(1) #=> 1
62
+ ```
63
+ In most cases this helps to make code less verbose and more semantic without having to rely on dispatch-via-method_missing (hooray!). If you're sticking side-effecting code in these blocks, though, weird things could potentially happen unless you're careful. If you want to avoid this behaviour, just be sure to give parameters to your blocks.
64
+
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ $LOAD_PATH << File.expand_path('../lib', __FILE__)
2
+ require 'mallow'
3
+ require 'rake'
4
+ require 'graham/rake_task'
5
+
6
+ Graham::RakeTask.new
7
+ task default: :test
8
+
9
+ namespace :gem do
10
+ task :build do
11
+ sh "gem b mallow.gemspec"
12
+ end
13
+ task :install do
14
+ sh "gem i #{Dir.glob('mallow-*.gem').sort.last}"
15
+ end
16
+ end
17
+ task gem: %w{ gem:build gem:install }
18
+
data/lib/mallow/dsl.rb ADDED
@@ -0,0 +1,104 @@
1
+ module Mallow
2
+ class DSL
3
+ attr_reader :core, :actions, :conditions
4
+
5
+ def self.build
6
+ yield (dsl = new)
7
+ dsl.finish!
8
+ end
9
+
10
+ def initialize
11
+ @core = Core.new
12
+ reset!
13
+ end
14
+
15
+ def where(&b); push b, :conditions end
16
+ def to(&b); push b, :actions end
17
+ def and(&b); push b end
18
+
19
+ def *; where {true} end
20
+ def a(c); where {|e| e.is_a? c} end
21
+ def this(o); where {|e| e == o} end
22
+ def size(n); where {|e| e.size==n rescue false} end
23
+ def with_key(k); where {|e| e.has_key?(k) rescue false} end
24
+
25
+ def and_hashify_with_keys(*ks); to {|e| Hash[ks.zip e]} end
26
+ def and_hashify_with_values(*vs); to {|e| Hash[e.zip vs]} end
27
+ def with_metadata(d={}); to {|e| Meta.new e, d} end
28
+
29
+ def to_nil; to{nil} end
30
+ def to_true; to{true} end
31
+ def to_false; to{false} end
32
+ def to_self; to{self} end
33
+
34
+ def tuple(n); a(Array).size(n) end
35
+ def and_make(o,s=false); and_send(:new,o,s) end
36
+
37
+ def and_send(msg, obj, splat = false)
38
+ to {|e| splat ? obj.send(msg, *e) : obj.send(msg, e)}
39
+ end
40
+
41
+ alias an a
42
+ alias ^ with_metadata
43
+ alias anything *
44
+ alias and_hashify_with and_hashify_with_keys
45
+ alias and_make_a and_make
46
+ alias and_make_an and_make
47
+ alias a_tuple tuple
48
+ alias such_that where
49
+ alias of_size size
50
+ alias to_itself to_self
51
+
52
+ def finish!
53
+ in_conds? ? to_self.finish! : rule!.core
54
+ end
55
+
56
+ # Checks for three forms:
57
+ # * (a|an)_(<thing>) with no args
58
+ # * (with|of)_(<msg>) with one arg, which tests <match>.send(<msg>) == arg
59
+ # * to_(<msg>) with any args, which resolves to <match>.send(<msg>) *args
60
+ def method_missing(msg, *args)
61
+ case msg.to_s
62
+ when /^(a|an)_(.+)$/
63
+ args.empty??
64
+ (a(Object.const_get $2.split(?_).map(&:capitalize).join) rescue super) :
65
+ super
66
+ when /^(with|of)_(.+)$/
67
+ args.size == 1 ?
68
+ where {|e| e.send($2) == args.first rescue false} :
69
+ super
70
+ when /^to_(.+)$/
71
+ to {|e| e.send $1, *args}
72
+ else
73
+ super
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def in_conds?
80
+ actions.empty?
81
+ end
82
+
83
+ def rule!
84
+ core << Rule::Builder[conditions, actions]
85
+ reset!
86
+ end
87
+
88
+ def push(p, loc = in_conds? ? :conditions : :actions)
89
+ rule! if loc == :conditions and not in_conds?
90
+ send(loc) << preproc(p)
91
+ self
92
+ end
93
+
94
+ def preproc(p)
95
+ p.parameters.empty? ? proc {|e| e.instance_eval &p} : p
96
+ end
97
+
98
+ def reset!
99
+ @conditions, @actions = Matcher.new, Transformer.new
100
+ self
101
+ end
102
+ end
103
+ end
104
+
@@ -0,0 +1,55 @@
1
+ module Mallow
2
+ # Rule monad(ish) encapsulating "execute the first rule whose conditions
3
+ # pass" logic.
4
+ class Rule < Struct.new :matcher, :transformer, :val
5
+ class << self
6
+ def return(v); new Matcher.new, Transformer.new, v end
7
+ end
8
+ # Curried proc for building procs for binding rules. If this were Haskell
9
+ # its type signature might vaguely resemble:
10
+ # Elt e => [e -> Bool] -> [e -> e] -> e -> Maybe (Meta e)
11
+ Builder = ->(cs, as, e) {Rule.new cs, as, e }.curry
12
+ # Behaves like an inverted Maybe: return self if match succeeds, otherwise
13
+ # attempt another match.
14
+ def bind(rule_proc); matcher === val ? self : rule_proc[val] end
15
+ def return(val); Rule.new matcher, transformer, val end
16
+ def unwrap!; matcher === val ? transformer >> val : dx end
17
+ private
18
+ def dx
19
+ raise DeserializationException, "No rule matches #{val}:#{val.class}"
20
+ end
21
+ end
22
+ # Wrapper monad(ish) for successful matches that allows the user to
23
+ # transparently store and access metadata across binds.
24
+ class Meta < Hash
25
+ attr_reader :val
26
+ # Curried proc that takes a proc and an object, calls the proc with the
27
+ # object, and wraps the return value in a Meta if it wasn't one already.
28
+ Builder = ->(p,e) { Meta === (e=p[e]) ? e : Meta.return(e) }.curry
29
+ def initialize(obj,md={})
30
+ @val = obj
31
+ merge! md
32
+ end
33
+ # Calls argument with the wrapped object and reverse-merges its metadata
34
+ # into that of the return value.
35
+ def bind(meta_proc); meta_proc[val].merge(self) {|k,o,n| o} end
36
+ class << self; alias return new end
37
+ end
38
+
39
+ # Container for rule conditions
40
+ class Matcher < Array
41
+ # Checks argument against all conditions; returns false if no conditions
42
+ # are present
43
+ def ===(e); any? and all? {|t| t[e]} end
44
+ end
45
+ # Container for rule actions
46
+ class Transformer < Array
47
+ # Threads argument through actions
48
+ def >>(e); reduce(Meta.return(e),:bind) end
49
+ # Wraps argument using Meta::proc
50
+ def <<(p); super Meta::Builder[p] end
51
+ alias push <<
52
+ end
53
+
54
+ end
55
+
@@ -0,0 +1,4 @@
1
+ module Mallow
2
+ # Mainly for show.
3
+ VERSION = '0.0.2'
4
+ end
data/lib/mallow.rb ADDED
@@ -0,0 +1,40 @@
1
+ $LOAD_PATH << File.dirname(__FILE__)
2
+ require 'mallow/version'
3
+ require 'mallow/monads'
4
+ require 'mallow/dsl'
5
+ module Mallow
6
+ class DeserializationException < StandardError; end
7
+
8
+ class Core < Array
9
+ def fluff(es)
10
+ _fluff(es).map &:val
11
+ end
12
+ def fluff1(e)
13
+ _fluff1(e).val
14
+ end
15
+ def _fluff(es)
16
+ es.map {|e| _fluff1 e}
17
+ end
18
+ def _fluff1(e)
19
+ reduce(Rule.return(e),:bind).unwrap!
20
+ end
21
+ class << self
22
+ # aka Mallow::DSL::build
23
+ def build(&b); DSL.build &b end
24
+ end
25
+ end
26
+
27
+ class << self
28
+ # aka Mallow::Core::build
29
+ def fluff(&b); Core.build(&b) end
30
+ # Defines a class method <sym> on <klass> to deserialize its argument (as
31
+ # with Core#fluff) using the Core generated by passing the supplied block
32
+ # to Mallow.fluff
33
+ def engulf(klass, sym=:fluff, &b)
34
+ mtd, mod = Mallow.fluff(&b), Module.new
35
+ mod.send(:define_method, sym) {|d| mtd.fluff d}
36
+ klass.extend mod
37
+ end
38
+ end
39
+ end
40
+
data/mallow.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
2
+ require 'rake'
3
+ require 'mallow/version'
4
+
5
+ mallow = Gem::Specification.new do |spec|
6
+ spec.name = 'mallow'
7
+ spec.version = Mallow::VERSION
8
+ spec.author = 'feivel jellyfish'
9
+ spec.email = 'feivel@sdf.org'
10
+ spec.files = FileList['mallow.gemspec','README.md','lib/**/*.rb']
11
+ spec.test_files = FileList['Rakefile','test/**/*.rb']
12
+ spec.homepage = 'http://github.com/gwentacle/mallow'
13
+ spec.summary = 'Tiny universal data pattern matcher / dispatcher'
14
+ spec.description = 'Tiny universal data pattern matcher / dispatcher'
15
+ spec.add_development_dependency 'graham'
16
+ end
17
+
@@ -0,0 +1,38 @@
1
+ class Graham::Cases
2
+ def initialize
3
+ @mallow1 = Mallow.fluff do |match|
4
+ match.a_string.to_upcase
5
+ match.a_tuple(3).to {|a,b,c| a+b+c}
6
+ match.a_fixnum.to {self*2}
7
+ end
8
+
9
+ @xmallow = Mallow.fluff do |match|
10
+ match.a(Fixnum).to {self/0}
11
+ match.a(String).to {self/self}
12
+ match.*.to {}
13
+ match.a_symbol.to {UNDEFINED}
14
+ end
15
+ end
16
+
17
+ def Case1
18
+ @mallow1.fluff [ 99, 'asdf', 'qwer', [5,5,5], 47, %w{cool story bro} ]
19
+ end
20
+
21
+ def XCase1
22
+ @xmallow.fluff1 1
23
+ end
24
+ def XCase2
25
+ @xmallow.fluff1 ?1
26
+ end
27
+ def XCase3
28
+ @xmallow.fluff1 :ok
29
+ end
30
+ end
31
+
32
+ Graham.pp {|that|
33
+ that.Case1.returns_an(Array).that_is [ 198, 'ASDF', 'QWER', 15, 94, 'coolstorybro' ]
34
+ that.XCase1.raises_a ZeroDivisionError
35
+ that.XCase2.raises_a NoMethodError
36
+ that.XCase3.does_not_raise_an_exception
37
+ }
38
+
@@ -0,0 +1,48 @@
1
+ class Graham::MonadLaws
2
+ def RuleLeftIdentity
3
+ rule = Mallow::Rule.return 350_000_000
4
+ f = Mallow::Rule::Builder[Mallow::Matcher.new][Mallow::Transformer.new]
5
+ rule.bind(f) == f[rule.val]
6
+ end
7
+
8
+ def RuleRightIdentity
9
+ rule = Mallow::Rule.return /thing/
10
+ rule == rule.bind(->(v){Mallow::Rule.return v})
11
+ end
12
+
13
+ def RuleAssociativity
14
+ rule = Mallow::Rule.return %w{qqq}
15
+ f = Mallow::Rule::Builder[Mallow::Matcher.new][Mallow::Transformer.new]
16
+ g = Mallow::Rule::Builder[Mallow::Matcher.new][Mallow::Transformer.new]
17
+ rule.bind(f).bind(g) == rule.bind(->(v){f[v].bind g})
18
+ end
19
+
20
+ def MetaLeftIdentity
21
+ meta = Mallow::Meta.return RUBY_VERSION
22
+ f = ->(v) {Mallow::Meta.return v, test: :data}
23
+ meta.bind(f) == f[meta.val]
24
+ end
25
+
26
+ def MetaRightIdentity
27
+ meta = Mallow::Meta.return Mallow
28
+ meta == meta.bind(->(v){Mallow::Meta.return v})
29
+ end
30
+
31
+ def MetaAssociativity
32
+ meta = Mallow::Meta.return __FILE__
33
+ f = ->(v){Mallow::Meta.return v, Florence: 'Nightingale'}
34
+ g = ->(v){Mallow::Meta.return v, 'Punk' => :Rock}
35
+ meta.bind(f).bind(g) == meta.bind(->(v){f[v].bind(g)})
36
+ end
37
+ end
38
+
39
+ Graham.pp(:MonadLaws) {|that|
40
+ that.RuleLeftIdentity.is true
41
+ that.RuleRightIdentity.is true
42
+ that.RuleAssociativity.is true
43
+
44
+ that.MetaLeftIdentity.is true
45
+ that.MetaRightIdentity.is true
46
+ that.MetaAssociativity.is true
47
+ }
48
+
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mallow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - feivel jellyfish
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: graham
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Tiny universal data pattern matcher / dispatcher
31
+ email: feivel@sdf.org
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - mallow.gemspec
37
+ - README.md
38
+ - lib/mallow.rb
39
+ - lib/mallow/version.rb
40
+ - lib/mallow/dsl.rb
41
+ - lib/mallow/monads.rb
42
+ - Rakefile
43
+ - test/case/mallow.rb
44
+ - test/unit/monad.rb
45
+ homepage: http://github.com/gwentacle/mallow
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.23
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Tiny universal data pattern matcher / dispatcher
69
+ test_files:
70
+ - Rakefile
71
+ - test/case/mallow.rb
72
+ - test/unit/monad.rb