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 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: