hard_boiled 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hard-boiled.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
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
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -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,13 @@
1
+ class Array
2
+ # Synonym for #empty?
3
+ def blank?
4
+ self.empty?
5
+ end
6
+ end
7
+
8
+ class NilClass
9
+ # Synonym for #nil?
10
+ def blank?
11
+ self.nil?
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module HardBoiled
2
+ VERSION = "0.2.1"
3
+ end
@@ -0,0 +1,3 @@
1
+ module HardBoiled
2
+ autoload :Presenter, 'hard-boiled/presenter'
3
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'hard_boiled'
5
+
6
+ require 'ostruct'
7
+
8
+ RSpec.configure do |config|
9
+ end
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: