elastic_attributes 0.1.0
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/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
|
+
|