elastic_attributes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +5 -0
- data/README.rdoc +43 -0
- data/Rakefile +14 -0
- data/elastic_attributes.gemspec +30 -0
- data/lib/elastic_attributes.rb +116 -0
- data/spec/elastic_attributes_spec.rb +130 -0
- metadata +79 -0
data/Manifest
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
= Elastic attributes
|
2
|
+
|
3
|
+
Flexible attribute mapper. Input any ruby structure, map sub-structures to specified classes.
|
4
|
+
|
5
|
+
Can be used to map decoded JSON structures to objects, e.g. with document-oriented databases or JSON APIs.
|
6
|
+
|
7
|
+
== Examples
|
8
|
+
|
9
|
+
class Person
|
10
|
+
include ElasticAttributes
|
11
|
+
attribute :name, :is_default => true
|
12
|
+
end
|
13
|
+
|
14
|
+
class City
|
15
|
+
include ElasticAttributes
|
16
|
+
attribute :name, :is_default => true
|
17
|
+
attribute :mayor, Person
|
18
|
+
end
|
19
|
+
|
20
|
+
class Country
|
21
|
+
include ElasticAttributes
|
22
|
+
attribute :name, :is_default => true
|
23
|
+
attribute :cities, [Array, City] # Array of Cities
|
24
|
+
end
|
25
|
+
|
26
|
+
Country.from( 'Hungary' )
|
27
|
+
Country.from( {'name' => 'Hungary'} )
|
28
|
+
Country.from( {'name' => 'Hungary', 'cities' => ['Budapest', 'Miskolc', 'Debrecen']} )
|
29
|
+
Country.from( {'name' => 'Hungary', 'cities' => [{'name' => 'Budapest', 'mayor' => 'Demszky Gabor'},
|
30
|
+
'Miskolc',
|
31
|
+
'Debrecen']} )
|
32
|
+
City.from( {'name' => 'Budapest', 'mayor' => 'Demszky Gabor'} )
|
33
|
+
city = City.from( {'name' => 'Budapest', 'mayor' => {'name' => 'Demszky Gabor'}} )
|
34
|
+
|
35
|
+
city.encode # => {"name"=>"Budapest", "mayor"=>"Demszky Gabor"}
|
36
|
+
|
37
|
+
== Installation
|
38
|
+
|
39
|
+
gem install elastic_attributes
|
40
|
+
|
41
|
+
== License
|
42
|
+
|
43
|
+
http://sam.zoy.org/wtfpl/
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
|
5
|
+
Echoe.new('elastic_attributes', '0.1.0') do |p|
|
6
|
+
p.description = "Flexible attribute mapping"
|
7
|
+
p.url = "http://github.com/bagilevi/elastic_attributes"
|
8
|
+
p.author = "Levente Bagi"
|
9
|
+
p.email = "bagilevi@gmail.com"
|
10
|
+
p.ignore_pattern = []
|
11
|
+
p.development_dependencies = []
|
12
|
+
end
|
13
|
+
|
14
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{elastic_attributes}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Levente Bagi"]
|
9
|
+
s.date = %q{2010-09-26}
|
10
|
+
s.description = %q{Flexible attribute mapping}
|
11
|
+
s.email = %q{bagilevi@gmail.com}
|
12
|
+
s.extra_rdoc_files = ["README.rdoc", "lib/elastic_attributes.rb"]
|
13
|
+
s.files = ["Manifest", "README.rdoc", "Rakefile", "lib/elastic_attributes.rb", "spec/elastic_attributes_spec.rb", "elastic_attributes.gemspec"]
|
14
|
+
s.homepage = %q{http://github.com/bagilevi/elastic_attributes}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Elastic_attributes", "--main", "README.rdoc"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{elastic_attributes}
|
18
|
+
s.rubygems_version = %q{1.3.7}
|
19
|
+
s.summary = %q{Flexible attribute mapping}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
else
|
27
|
+
end
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module ElasticAttributes
|
2
|
+
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
# Define an attribute in the current class.
|
6
|
+
# attribute :image, Image # Image.from(data) will be called on the input['image']
|
7
|
+
# attribute :images, [Array, Image] # array of images - Image.from(data) will be called on all items in input['images'] array
|
8
|
+
# attribute :text, :is_default => true # If the input is not a hash, it will be assigned to this attribute, and all other attributes will be nil
|
9
|
+
def attribute(name, type_or_options = nil, options = {})
|
10
|
+
if type_or_options.is_a?(Hash)
|
11
|
+
options = type_or_options
|
12
|
+
else
|
13
|
+
options[:type] = type_or_options if type_or_options
|
14
|
+
end
|
15
|
+
attr_accessor name
|
16
|
+
self.attributes ||= {}
|
17
|
+
self.attributes[name] = options
|
18
|
+
self.default_attribute = name if options[:is_default]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create an object from data
|
22
|
+
def from data
|
23
|
+
obj = new
|
24
|
+
obj.decode data
|
25
|
+
obj
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# Unserialize data: map it to the current object attributes.
|
31
|
+
def decode data
|
32
|
+
if data.is_a? Hash
|
33
|
+
self.class.attributes.each do |name, options|
|
34
|
+
send("#{name}=", processed_data(data[name] || data[name.to_s], options))
|
35
|
+
end
|
36
|
+
elsif name = self.class.default_attribute
|
37
|
+
options = self.class.attributes[name]
|
38
|
+
send("#{name}=", processed_data(data, options))
|
39
|
+
else
|
40
|
+
raise ArgumentError.new("data is not a Hash (it\'s a #{data.class}) and default attribute not specified")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Serialize data
|
45
|
+
def encode
|
46
|
+
if (name = self.class.default_attribute) && ! (self.class.attributes.keys - [name]).any?{|n|send(n)}
|
47
|
+
send(name)
|
48
|
+
else
|
49
|
+
Hash[(
|
50
|
+
self.class.attributes.keys.map do |name|
|
51
|
+
value = encoded_data(send(name), self.class.attributes[name])
|
52
|
+
[name.to_s, value] if value
|
53
|
+
end.compact
|
54
|
+
)]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def processed_data(data, options)
|
61
|
+
if data.nil?
|
62
|
+
nil
|
63
|
+
elsif options[:type]
|
64
|
+
types = Array(options[:type])
|
65
|
+
main_type = types.first
|
66
|
+
if main_type == Array && types.size > 1
|
67
|
+
data.map{|data_item| types[1].from data_item }
|
68
|
+
elsif main_type.respond_to? :from
|
69
|
+
main_type.from data
|
70
|
+
elsif main_type == Time
|
71
|
+
require 'time'
|
72
|
+
Time.parse(data)
|
73
|
+
elsif main_type == Hash
|
74
|
+
Hash[data]
|
75
|
+
else
|
76
|
+
main_type.new data
|
77
|
+
end
|
78
|
+
else
|
79
|
+
data
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def encoded_data(data, options)
|
84
|
+
if data.nil?
|
85
|
+
nil
|
86
|
+
elsif options[:type]
|
87
|
+
types = Array(options[:type])
|
88
|
+
main_type = types.first
|
89
|
+
if main_type == Array && types.size > 1
|
90
|
+
data.map{|data_item| data_item.encode }
|
91
|
+
elsif data.respond_to? :encode
|
92
|
+
data.encode
|
93
|
+
elsif main_type == Time
|
94
|
+
data.to_s
|
95
|
+
elsif main_type == Hash
|
96
|
+
data
|
97
|
+
else
|
98
|
+
data
|
99
|
+
end
|
100
|
+
else
|
101
|
+
data
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class << self
|
106
|
+
def included(klass)
|
107
|
+
klass.instance_eval do
|
108
|
+
# cattr_accessible
|
109
|
+
[:attributes, :default_attribute].each {|n|instance_eval"def #{n};@#{n};end; def #{n}=(v);@#{n}=v;end" }
|
110
|
+
end
|
111
|
+
klass.extend ElasticAttributes::ClassMethods
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/elastic_attributes'
|
3
|
+
|
4
|
+
class Person
|
5
|
+
include ElasticAttributes
|
6
|
+
attribute :name
|
7
|
+
end
|
8
|
+
|
9
|
+
class City
|
10
|
+
include ElasticAttributes
|
11
|
+
attribute :name
|
12
|
+
attribute :mayor, Person
|
13
|
+
end
|
14
|
+
|
15
|
+
class Country
|
16
|
+
include ElasticAttributes
|
17
|
+
attribute :name
|
18
|
+
attribute :cities, [Array, City] # Array of Cities
|
19
|
+
end
|
20
|
+
|
21
|
+
class Item
|
22
|
+
include ElasticAttributes
|
23
|
+
attribute :description, :is_default => true
|
24
|
+
attribute :notes
|
25
|
+
end
|
26
|
+
|
27
|
+
class List
|
28
|
+
include ElasticAttributes
|
29
|
+
attribute :items, [Array, Item]
|
30
|
+
end
|
31
|
+
|
32
|
+
class Apple
|
33
|
+
include ElasticAttributes
|
34
|
+
attribute :picked_at, Time
|
35
|
+
end
|
36
|
+
|
37
|
+
class Collection
|
38
|
+
include ElasticAttributes
|
39
|
+
attribute :things, Array
|
40
|
+
end
|
41
|
+
|
42
|
+
class House
|
43
|
+
include ElasticAttributes
|
44
|
+
attribute :tenants, Hash
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
describe ElasticAttributes do
|
49
|
+
|
50
|
+
describe "decoding" do
|
51
|
+
|
52
|
+
it "should handle simple attribute" do
|
53
|
+
person = Person.from({'name' => 'Andrea'})
|
54
|
+
person.name.should == 'Andrea'
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should create an object with the given type" do
|
58
|
+
city = City.from({'name' => 'Budapest', 'mayor' => {'name' => 'Gábor Demszky'}})
|
59
|
+
city.name.should == 'Budapest'
|
60
|
+
city.mayor.should be_a Person
|
61
|
+
city.mayor.name.should == 'Gábor Demszky'
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should create an object with missing custom-typed attribute" do
|
65
|
+
city = City.from({'name' => 'Budapest'})
|
66
|
+
city.name.should == 'Budapest'
|
67
|
+
city.mayor.should be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should create an array of objects with a given type" do
|
71
|
+
country = Country.from({'name' => 'Hungary', 'cities' => [{'name' => 'Budapest'}, {'name' => 'Miskolc'}]})
|
72
|
+
country.cities.each{|city| city.should be_a City}
|
73
|
+
country.cities.first.name.should == 'Budapest'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should use default attribute if the source data is not a hash" do
|
77
|
+
list = List.from({'items' => ['buy milk',
|
78
|
+
'feed cat',
|
79
|
+
{'description' => 'water plants', 'notes' => 'in the room too'}
|
80
|
+
]})
|
81
|
+
list.items.each{|item| item.should be_a Item}
|
82
|
+
list.items.first.description.should == 'buy milk'
|
83
|
+
list.items.first.notes.should be_nil
|
84
|
+
list.items.last.description.should == 'water plants'
|
85
|
+
list.items.last.notes.should == 'in the room too'
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should handle Time" do
|
89
|
+
apple = Apple.from({'picked_at' => '2010-09-25 15:15'})
|
90
|
+
apple.picked_at.should == Time.parse('2010-09-25 15:15')
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should handle Array" do
|
94
|
+
a = ['foo', :bar, {:name => 'John'}]
|
95
|
+
collection = Collection.from({'things' => a})
|
96
|
+
collection.things.should == a
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should handle Hash" do
|
100
|
+
h = {:basement => 'John', :floor1 => 'Mary'}
|
101
|
+
house = House.from({'tenants' => h})
|
102
|
+
house.tenants.should == h
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "encoding" do
|
107
|
+
it "should be the reverse of decoding" do
|
108
|
+
[
|
109
|
+
[Person, {'name' => 'Andrea'}],
|
110
|
+
[City, {'name' => 'Budapest', 'mayor' => {'name' => 'Gábor Demszky'}}],
|
111
|
+
[City, {'name' => 'Budapest'}],
|
112
|
+
[Country, {'name' => 'Hungary', 'cities' => [{'name' => 'Budapest'}, {'name' => 'Miskolc'}]}],
|
113
|
+
[List, {'items' => ['buy milk',
|
114
|
+
'feed cat',
|
115
|
+
{'description' => 'water plants', 'notes' => 'in the room too'}
|
116
|
+
]}],
|
117
|
+
[Collection, {'things' => ['foo', :bar, {:name => 'John'}]}],
|
118
|
+
[House, {'tenants' => {:basement => 'John', :floor1 => 'Mary'}}]
|
119
|
+
].each do |klass, input|
|
120
|
+
klass.from(input).encode.should == input
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should encode time" do
|
125
|
+
apple = Apple.from({'picked_at' => '2010-09-25 15:15'})
|
126
|
+
apple.encode['picked_at'].should be_a String
|
127
|
+
Time.parse(apple.encode['picked_at']).should == Time.parse('2010-09-25 15:15')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elastic_attributes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Levente Bagi
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-09-26 00:00:00 +03:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Flexible attribute mapping
|
23
|
+
email: bagilevi@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.rdoc
|
30
|
+
- lib/elastic_attributes.rb
|
31
|
+
files:
|
32
|
+
- Manifest
|
33
|
+
- README.rdoc
|
34
|
+
- Rakefile
|
35
|
+
- lib/elastic_attributes.rb
|
36
|
+
- spec/elastic_attributes_spec.rb
|
37
|
+
- elastic_attributes.gemspec
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://github.com/bagilevi/elastic_attributes
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --line-numbers
|
45
|
+
- --inline-source
|
46
|
+
- --title
|
47
|
+
- Elastic_attributes
|
48
|
+
- --main
|
49
|
+
- README.rdoc
|
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: 11
|
67
|
+
segments:
|
68
|
+
- 1
|
69
|
+
- 2
|
70
|
+
version: "1.2"
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project: elastic_attributes
|
74
|
+
rubygems_version: 1.3.7
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: Flexible attribute mapping
|
78
|
+
test_files: []
|
79
|
+
|