hard_boiled 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +6 -0
- data/README.md +40 -0
- data/Rakefile +2 -0
- data/hard_boiled.gemspec +27 -0
- data/lib/hard_boiled/blank.rb +13 -0
- data/lib/hard_boiled/extract_options.rb +32 -0
- data/lib/hard_boiled/presenter.rb +107 -0
- data/lib/hard_boiled/version.rb +3 -0
- data/lib/hard_boiled.rb +3 -0
- data/spec/presenter_spec.rb +215 -0
- data/spec/spec_helper.rb +9 -0
- metadata +63 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
## HardBoiled
|
2
|
+
|
3
|
+
simply define mapping from you model to a simple hash. For those who worked with [thoughtbot](http://thoughtbot.com)'s [factory girl](http://github.com/thoughtbot/factory_girl) the DSL should be familiar.
|
4
|
+
|
5
|
+
### Installation
|
6
|
+
|
7
|
+
gem install hard-boiled
|
8
|
+
|
9
|
+
### Usage
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'hard-boiled'
|
13
|
+
|
14
|
+
egg = OpenStruct.new({
|
15
|
+
:boil_time => 7,
|
16
|
+
:temperature => 99,
|
17
|
+
:colour => "beige"
|
18
|
+
})
|
19
|
+
|
20
|
+
HardBoiled::Presenter.define egg do
|
21
|
+
time :from => :boil_time
|
22
|
+
colour
|
23
|
+
temperature :format => "%d ℃"
|
24
|
+
end # => { :time => 7, :temperature => "99 ℃", :colour => "beige" }
|
25
|
+
```
|
26
|
+
|
27
|
+
for more examples see the tests in the `spec` directory.
|
28
|
+
|
29
|
+
### Similar Projects
|
30
|
+
|
31
|
+
If _hard-boiled_ isn't your cup of tea, go and check out other ways to map models
|
32
|
+
to hashes (for data serialization):
|
33
|
+
|
34
|
+
* [Representative](https://github.com/mdub/representative)
|
35
|
+
* [Tokamak](https://github.com/abril/tokamak)
|
36
|
+
* [Builder](http://rubygems.org/gems/builder)
|
37
|
+
* [JSONify](https://github.com/bsiggelkow/jsonify)
|
38
|
+
* [Argonaut](https://github.com/jbr/argonaut)
|
39
|
+
* [JSON Builder](https://github.com/dewski/json_builder)
|
40
|
+
* [RABL](https://github.com/nesquena/rabl)
|
data/Rakefile
ADDED
data/hard_boiled.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "hard_boiled/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "hard_boiled"
|
7
|
+
s.version = HardBoiled::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Lennart Melzer"]
|
10
|
+
s.email = ["me@lmaa.name"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Get your models boiled down to plain hashes!}
|
13
|
+
s.description = %q{
|
14
|
+
HardBoiled helps you reducing your complex models (including their associations)
|
15
|
+
down to simple hashes usable for serialization into JSON or XML.
|
16
|
+
|
17
|
+
It leverages a DSL similar to thoughtbot's FactoryGirl
|
18
|
+
to make mappings maintainable and pain-free.
|
19
|
+
}
|
20
|
+
|
21
|
+
s.rubyforge_project = "hard_boiled"
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Somebody said, I won't depend on `ActiveSupport` for one method. Ok
|
2
|
+
# these are two but still… taken from the
|
3
|
+
# [rails project](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/extract_options.rb)
|
4
|
+
class Hash
|
5
|
+
# By default, only instances of Hash itself are extractable.
|
6
|
+
# Subclasses of Hash may implement this method and return
|
7
|
+
# true to declare themselves as extractable. If a Hash
|
8
|
+
# is extractable, Array#extract_options! pops it from
|
9
|
+
# the Array when it is the last element of the Array.
|
10
|
+
def extractable_options?
|
11
|
+
instance_of?(Hash)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Array
|
16
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
17
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
18
|
+
#
|
19
|
+
# def options(*args)
|
20
|
+
# args.extract_options!
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# options(1, 2) # => {}
|
24
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
25
|
+
def extract_options!
|
26
|
+
if last.is_a?(Hash) && last.extractable_options?
|
27
|
+
pop
|
28
|
+
else
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module HardBoiled
|
2
|
+
# Boilerplate
|
3
|
+
require File.dirname(__FILE__)+'/extract_options' unless {}.respond_to?(:extractable_options?)
|
4
|
+
require File.dirname(__FILE__)+'/blank' unless nil.respond_to?(:blank?)
|
5
|
+
|
6
|
+
# This class pretty much resembles what Thoughtbot did in
|
7
|
+
# [FactoryGirl's DefinitionProxy](https://github.com/thoughtbot/factory_girl/blob/master/lib/factory_girl/definition_proxy.rb)
|
8
|
+
# although it just reduces a `class` to a simple `Hash`
|
9
|
+
class Presenter
|
10
|
+
class MissingFilterError < StandardError; end
|
11
|
+
UNPROXIED_METHODS = %w(__send__ __id__ nil? respond_to? class send object_id extend instance_eval initialize block_given? raise)
|
12
|
+
|
13
|
+
(instance_methods + private_instance_methods).each do |m|
|
14
|
+
undef_method m unless UNPROXIED_METHODS.include? m
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :subject, :parent_subject
|
18
|
+
|
19
|
+
def self.define *args, &block
|
20
|
+
# if I could only remove the duplicate `obj`
|
21
|
+
obj = new(*args)
|
22
|
+
obj.instance_eval(&block)
|
23
|
+
obj.to_hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize *args
|
27
|
+
@options = args.extract_options!
|
28
|
+
@subject, @parent_subject = args
|
29
|
+
@hash = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Decide whether the given trait is being needed
|
34
|
+
#
|
35
|
+
# @param [Symbol] name the identifier for a trait, like :profile
|
36
|
+
# @param [Hash{:only => Array, :except => Array}]
|
37
|
+
# @return [true, false] dependening on in- or exclusion of this trait
|
38
|
+
def with_trait name, &block
|
39
|
+
if (@options[:except].blank? || !@options[:except].include?(name)) &&
|
40
|
+
(@options[:only].blank? || @options[:only].include?(name))
|
41
|
+
self.instance_eval(&block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
@hash
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def method_missing id, *args, &block
|
51
|
+
options = args.extract_options!
|
52
|
+
params = options[:params]
|
53
|
+
value =
|
54
|
+
if options[:nil]
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
if static = args.shift
|
58
|
+
static
|
59
|
+
else
|
60
|
+
object = options[:parent] ? parent_subject : subject
|
61
|
+
method_name = options[:from] || id
|
62
|
+
if params
|
63
|
+
object.__send__ method_name, *params
|
64
|
+
else
|
65
|
+
object.__send__ method_name
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
@hash[id] =
|
70
|
+
if block_given?
|
71
|
+
if value.kind_of? Array
|
72
|
+
value.map do |v|
|
73
|
+
self.class.define(v, self.subject, &block)
|
74
|
+
end
|
75
|
+
else
|
76
|
+
self.class.define(value, self.subject, &block)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
__set_defaults __format_value(__apply_filters(value, options), options), options
|
80
|
+
end
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def __set_defaults value, options
|
85
|
+
if value.nil? and default = options[:default]
|
86
|
+
default
|
87
|
+
else
|
88
|
+
value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def __apply_filters value, options
|
93
|
+
if filters = options[:filters]
|
94
|
+
filters.inject(value) { |result, filter|
|
95
|
+
raise MissingFilterError, filter.to_s unless self.respond_to?(filter)
|
96
|
+
self.__send__(filter, result)
|
97
|
+
}
|
98
|
+
else
|
99
|
+
value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def __format_value value, options
|
104
|
+
(format = options[:format]) ? format % value : value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/hard_boiled.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
module MyFilters
|
4
|
+
def upcase value
|
5
|
+
value.upcase
|
6
|
+
end
|
7
|
+
|
8
|
+
def twice_and_a_half value
|
9
|
+
value * 2.5
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Filterable < HardBoiled::Presenter
|
14
|
+
include MyFilters
|
15
|
+
end
|
16
|
+
|
17
|
+
class Calculator
|
18
|
+
def add a, b
|
19
|
+
a + b
|
20
|
+
end
|
21
|
+
|
22
|
+
def zero
|
23
|
+
0
|
24
|
+
end
|
25
|
+
|
26
|
+
def negative
|
27
|
+
-100
|
28
|
+
end
|
29
|
+
|
30
|
+
def tax val = 0.0
|
31
|
+
((val + 1) * 2700).to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe HardBoiled::Presenter do
|
36
|
+
let(:egg) {
|
37
|
+
OpenStruct.new({:temperature => 25, :boil_time => 7, :colour => "white"})
|
38
|
+
}
|
39
|
+
|
40
|
+
let(:conventional_egg) {
|
41
|
+
OpenStruct.new({:temperature => 25, :boil_time => 5,
|
42
|
+
:colour => "brownish", :organic => false})
|
43
|
+
}
|
44
|
+
|
45
|
+
it "should produce correct hash" do
|
46
|
+
definition = described_class.define(egg) do
|
47
|
+
colour
|
48
|
+
time :from => :boil_time
|
49
|
+
consumer "Lennart"
|
50
|
+
end
|
51
|
+
|
52
|
+
definition.should == {
|
53
|
+
:colour => "white",
|
54
|
+
:time => 7,
|
55
|
+
:consumer => "Lennart"
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
context :paramified do
|
60
|
+
it "should pass params to member function" do
|
61
|
+
definition = described_class.define(Calculator.new) do
|
62
|
+
negative
|
63
|
+
zero
|
64
|
+
add :params => [5, 2]
|
65
|
+
end
|
66
|
+
|
67
|
+
definition.should == {
|
68
|
+
:negative => -100,
|
69
|
+
:zero => 0,
|
70
|
+
:add => 7
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should pass param to member function" do
|
75
|
+
definition = described_class.define(Calculator.new) do
|
76
|
+
null :from => :zero
|
77
|
+
tax
|
78
|
+
sales :from => :tax, :params => 0.19
|
79
|
+
end
|
80
|
+
|
81
|
+
definition.should == {
|
82
|
+
:null => 0,
|
83
|
+
:tax => 2700,
|
84
|
+
:sales => 3213
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context :nested do
|
90
|
+
let(:egg_box) {
|
91
|
+
OpenStruct.new({
|
92
|
+
:eggs => [egg, conventional_egg],
|
93
|
+
:flavour => "extra tasty",
|
94
|
+
:packaged_at => "2011-11-22"
|
95
|
+
})
|
96
|
+
}
|
97
|
+
|
98
|
+
it "should allow nested objects" do
|
99
|
+
definition = Filterable.define egg_box do
|
100
|
+
contents :from => :eggs do
|
101
|
+
colour
|
102
|
+
time :from => :boil_time, :filters => [:twice_and_a_half], :format => "%.2f minutes"
|
103
|
+
taste :from => :flavour, :parent => true
|
104
|
+
consumer "Lennart", :filters => [:upcase]
|
105
|
+
"Return value has to be ignored"
|
106
|
+
end
|
107
|
+
|
108
|
+
date :from => :packaged_at, :format => "on %s"
|
109
|
+
end
|
110
|
+
|
111
|
+
definition.should == {
|
112
|
+
:contents => [
|
113
|
+
{
|
114
|
+
:colour => "white",
|
115
|
+
:time => "17.50 minutes",
|
116
|
+
:consumer => "LENNART",
|
117
|
+
:taste => "extra tasty"
|
118
|
+
},
|
119
|
+
{
|
120
|
+
:colour => "brownish",
|
121
|
+
:time => "12.50 minutes",
|
122
|
+
:consumer => "LENNART",
|
123
|
+
:taste => "extra tasty"
|
124
|
+
}
|
125
|
+
],
|
126
|
+
:date => "on 2011-11-22"
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context :filtering do
|
132
|
+
it "should apply filters" do
|
133
|
+
definition = Filterable.define egg do
|
134
|
+
colour :filters => [:upcase]
|
135
|
+
time :from => :boil_time
|
136
|
+
consumer "Lennart"
|
137
|
+
end
|
138
|
+
|
139
|
+
definition.should == {
|
140
|
+
:colour => "WHITE",
|
141
|
+
:time => 7,
|
142
|
+
:consumer => "Lennart"
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should raise on missing filter" do
|
147
|
+
expect {
|
148
|
+
definition = described_class.define egg do
|
149
|
+
colour :filters => [:upcase]
|
150
|
+
time :from => :boil_time
|
151
|
+
consumer "Lennart"
|
152
|
+
end
|
153
|
+
}.to raise_error(HardBoiled::Presenter::MissingFilterError)
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
context :defaults do
|
159
|
+
def with_defaults obj
|
160
|
+
Filterable.define obj do
|
161
|
+
colour :filters => [:upcase]
|
162
|
+
time :from => :boil_time
|
163
|
+
consumer "Lennart"
|
164
|
+
organic :default => true
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should fallback to defaults" do
|
169
|
+
with_defaults(egg).should == {
|
170
|
+
:colour => "WHITE",
|
171
|
+
:time => 7,
|
172
|
+
:consumer => "Lennart",
|
173
|
+
:organic => true
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should still use defined values" do
|
178
|
+
with_defaults(conventional_egg).should == {
|
179
|
+
:colour => "BROWNISH",
|
180
|
+
:time => 5,
|
181
|
+
:consumer => "Lennart",
|
182
|
+
:organic => false
|
183
|
+
}
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context :traits do
|
188
|
+
def with_traits obj, options = {}
|
189
|
+
Filterable.define(obj, options) {
|
190
|
+
with_trait(:instructions) {
|
191
|
+
with_trait(:timing) {
|
192
|
+
boil_time
|
193
|
+
}
|
194
|
+
temperature
|
195
|
+
}
|
196
|
+
with_trait(:presentation) {
|
197
|
+
colour
|
198
|
+
}
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
it "should just map instructions" do
|
203
|
+
with_traits(egg, :only => [:instructions]).should == {
|
204
|
+
:temperature => 25
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should map everything except for timing information" do
|
209
|
+
with_traits(egg, :except => [:timing]).should == {
|
210
|
+
:colour => "white",
|
211
|
+
:temperature => 25
|
212
|
+
}
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hard_boiled
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lennart Melzer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-24 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! "\n HardBoiled helps you reducing your complex models (including
|
15
|
+
their associations)\n down to simple hashes usable for serialization into JSON
|
16
|
+
or XML.\n\n It leverages a DSL similar to thoughtbot's FactoryGirl\n to make
|
17
|
+
mappings maintainable and pain-free.\n "
|
18
|
+
email:
|
19
|
+
- me@lmaa.name
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- .gitignore
|
25
|
+
- Gemfile
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- hard_boiled.gemspec
|
29
|
+
- lib/hard_boiled.rb
|
30
|
+
- lib/hard_boiled/blank.rb
|
31
|
+
- lib/hard_boiled/extract_options.rb
|
32
|
+
- lib/hard_boiled/presenter.rb
|
33
|
+
- lib/hard_boiled/version.rb
|
34
|
+
- spec/presenter_spec.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
homepage: ''
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project: hard_boiled
|
56
|
+
rubygems_version: 1.8.17
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Get your models boiled down to plain hashes!
|
60
|
+
test_files:
|
61
|
+
- spec/presenter_spec.rb
|
62
|
+
- spec/spec_helper.rb
|
63
|
+
has_rdoc:
|