hard-boiled 0.0.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.rb +3 -0
- data/lib/hard-boiled/extract_options.rb +32 -0
- data/lib/hard-boiled/presenter.rb +67 -0
- data/lib/hard-boiled/version.rb +5 -0
- data/spec/presenter_spec.rb +97 -0
- data/spec/spec_helper.rb +9 -0
- metadata +79 -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 = Hard::Boiled::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
|
data/lib/hard-boiled.rb
ADDED
@@ -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,67 @@
|
|
1
|
+
module HardBoiled
|
2
|
+
require File.dirname(__FILE__)+'/extract_options' unless {}.respond_to?(:extractable_options?)
|
3
|
+
|
4
|
+
# This class pretty much resembles what Thoughtbot did in
|
5
|
+
# [FactoryGirl's DefinitionProxy](https://github.com/thoughtbot/factory_girl/blob/master/lib/factory_girl/definition_proxy.rb)
|
6
|
+
# although it just reduces a `class` to a simple `Hash`
|
7
|
+
class Presenter
|
8
|
+
class MissingFilterError < StandardError; end
|
9
|
+
UNPROXIED_METHODS = %w(__send__ __id__ nil? respond_to? class send object_id extend instance_eval initialize block_given? raise)
|
10
|
+
|
11
|
+
(instance_methods + private_instance_methods).each do |m|
|
12
|
+
undef_method m unless UNPROXIED_METHODS.include? m
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :subject, :parent_subject
|
16
|
+
|
17
|
+
def self.define object, parent = nil, &block
|
18
|
+
new(object, parent).
|
19
|
+
instance_eval(&block).
|
20
|
+
to_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize subject, parent = nil
|
24
|
+
@subject = subject
|
25
|
+
@parent_subject = parent
|
26
|
+
@hash = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
@hash
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def method_missing id, *args, &block
|
35
|
+
options = args.extract_options!
|
36
|
+
value = options[:nil] ? nil : (args.shift || (options[:parent] ? parent_subject : subject).__send__(options[:from] || id))
|
37
|
+
@hash[id] =
|
38
|
+
if block_given?
|
39
|
+
if value.kind_of? Array
|
40
|
+
value.map do |v|
|
41
|
+
self.class.define(v, self.subject, &block)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
self.class.define(value, self.subject, &block)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
__format_value __apply_filters(value, options), options
|
48
|
+
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def __apply_filters value, options
|
53
|
+
if filters = options[:filters]
|
54
|
+
filters.inject(value) { |result, filter|
|
55
|
+
raise MissingFilterError unless self.respond_to?(filter)
|
56
|
+
self.__send__(filter, result)
|
57
|
+
}
|
58
|
+
else
|
59
|
+
value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def __format_value value, options
|
64
|
+
(format = options[:format]) ? format % value : value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,97 @@
|
|
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
|
+
describe HardBoiled::Presenter do
|
18
|
+
let(:egg) {
|
19
|
+
OpenStruct.new({:temperature => 25, :boil_time => 7, :colour => "white"})
|
20
|
+
}
|
21
|
+
|
22
|
+
it "should produce correct hash" do
|
23
|
+
definition = described_class.define egg do
|
24
|
+
colour
|
25
|
+
time :from => :boil_time
|
26
|
+
consumer "Lennart"
|
27
|
+
end
|
28
|
+
|
29
|
+
definition.should == {
|
30
|
+
:colour => "white",
|
31
|
+
:time => 7,
|
32
|
+
:consumer => "Lennart"
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
context :nested do
|
37
|
+
let(:egg_box) {
|
38
|
+
OpenStruct.new({
|
39
|
+
:eggs => [egg],
|
40
|
+
:flavour => "extra tasty",
|
41
|
+
:packaged_at => "2011-11-22"
|
42
|
+
})
|
43
|
+
}
|
44
|
+
|
45
|
+
it "should allow nested objects" do
|
46
|
+
definition = Filterable.define egg_box do
|
47
|
+
contents :from => :eggs do
|
48
|
+
colour
|
49
|
+
time :from => :boil_time, :filters => [:twice_and_a_half], :format => "%.2f minutes"
|
50
|
+
taste :from => :flavour, :parent => true
|
51
|
+
consumer "Lennart", :filters => [:upcase]
|
52
|
+
end
|
53
|
+
|
54
|
+
date :from => :packaged_at, :format => "on %s"
|
55
|
+
end
|
56
|
+
|
57
|
+
definition.should == {
|
58
|
+
:contents => [
|
59
|
+
{
|
60
|
+
:colour => "white",
|
61
|
+
:time => "17.50 minutes",
|
62
|
+
:consumer => "LENNART",
|
63
|
+
:taste => "extra tasty"
|
64
|
+
}
|
65
|
+
],
|
66
|
+
:date => "on 2011-11-22"
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context :filtering do
|
72
|
+
it "should apply filters" do
|
73
|
+
definition = Filterable.define egg do
|
74
|
+
colour :filters => [:upcase]
|
75
|
+
time :from => :boil_time
|
76
|
+
consumer "Lennart"
|
77
|
+
end
|
78
|
+
|
79
|
+
definition.should == {
|
80
|
+
:colour => "WHITE",
|
81
|
+
:time => 7,
|
82
|
+
:consumer => "Lennart"
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should raise on missing filter" do
|
87
|
+
expect {
|
88
|
+
definition = described_class.define egg do
|
89
|
+
colour :filters => [:upcase]
|
90
|
+
time :from => :boil_time
|
91
|
+
consumer "Lennart"
|
92
|
+
end
|
93
|
+
}.to raise_error(HardBoiled::Presenter::MissingFilterError)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hard-boiled
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Lennart Melzer
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-23 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: "\n HardBoiled helps you reducing your complex models (including their associations)\n down to simple hashes usable for serialization into JSON or XML.\n\n It leverages a DSL similar to thoughtbot's FactoryGirl \n to make mappings maintainable and pain-free.\n "
|
23
|
+
email:
|
24
|
+
- me@lmaa.name
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- README.md
|
35
|
+
- Rakefile
|
36
|
+
- hard-boiled.gemspec
|
37
|
+
- lib/hard-boiled.rb
|
38
|
+
- lib/hard-boiled/extract_options.rb
|
39
|
+
- lib/hard-boiled/presenter.rb
|
40
|
+
- lib/hard-boiled/version.rb
|
41
|
+
- spec/presenter_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: ""
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 3
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project: hard-boiled
|
73
|
+
rubygems_version: 1.4.2
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Get your models boiled down to plain hashes!
|
77
|
+
test_files:
|
78
|
+
- spec/presenter_spec.rb
|
79
|
+
- spec/spec_helper.rb
|