mallow 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +64 -0
- data/Rakefile +18 -0
- data/lib/mallow/dsl.rb +104 -0
- data/lib/mallow/monads.rb +55 -0
- data/lib/mallow/version.rb +4 -0
- data/lib/mallow.rb +40 -0
- data/mallow.gemspec +17 -0
- data/test/case/mallow.rb +38 -0
- data/test/unit/monad.rb +48 -0
- metadata +72 -0
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
|
+
|
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
|
+
|
data/test/case/mallow.rb
ADDED
@@ -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
|
+
|
data/test/unit/monad.rb
ADDED
@@ -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
|