ixtlan-babel 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ixtlan-babel (0.1.2)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ json_pure (1.6.1)
10
+ minitest (2.11.3)
11
+ rake (0.9.2.2)
12
+
13
+ PLATFORMS
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ ixtlan-babel!
18
+ json_pure (~> 1.6.1)
19
+ minitest (= 2.11.3)
20
+ rake (= 0.9.2.2)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Kristian Meier
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # vellam [![Build Status](https://secure.travis-ci.org/mkristian/babel.png)](http://travis-ci.org/mkristian/babel) #
2
+
3
+ rails comes with `to_json` and `to_xml` on models and you can give them an option map to control how the whole object tree gets serialized.
4
+
5
+ the first problem I had was that I needed serveral options map at different controllers/actions so I needed a place to store them. the model itself felt to be the wrong place.
6
+
7
+ the next problem was that I could include the result of a given method with `:methods => ['age']` but only on the root level of the object tree. but if I wanted to `age` method to be part of the serialization somewhere deep inside the object tree, it is not possible.
8
+
9
+ please have a look at **spec/filter_spec.rb** how to use it :)
@@ -0,0 +1,4 @@
1
+ require 'ixtlan/babel/serializer'
2
+ require 'ixtlan/babel/deserializer'
3
+ require 'ixtlan/babel/factory'
4
+ require 'ixtlan/babel/no_timestamp_serializer'
@@ -0,0 +1,65 @@
1
+ module Ixtlan
2
+ module Babel
3
+ class Deserializer
4
+
5
+ def initialize(model_class)
6
+ @model_class = model_class
7
+ end
8
+
9
+ private
10
+
11
+ def self.filter
12
+ @filter ||= HashFilter.new
13
+ end
14
+
15
+ def filter
16
+ @filter ||= self.class.filter.dup
17
+ end
18
+
19
+ protected
20
+
21
+ def self.default_context_key(default)
22
+ filter.default_context_key(default)
23
+ end
24
+
25
+ def self.add_context(key, options = {})
26
+ filter[key] = options
27
+ end
28
+
29
+ public
30
+
31
+ def use(context_or_options)
32
+ filter.use(context_or_options)
33
+ self
34
+ end
35
+
36
+ def from_hash(data, options = nil)
37
+ filter.use(options) if options
38
+ if root = filter.options[:root]
39
+ if data.is_a? Array
40
+ root = root.to_s
41
+ data.collect{ |d| @model_class.new(filter.filter(d[root])) }
42
+ else
43
+ @model_class.new(filter.filter(data[root.to_s]))
44
+ end
45
+ else
46
+ if data.is_a? Array
47
+ data.collect{ |d| @model_class.new(filter.filter(d)) }
48
+ else
49
+ @model_class.new(filter.filter(data))
50
+ end
51
+ end
52
+ end
53
+
54
+ def from_json(json, options = nil)
55
+ data = JSON.parse(json)
56
+ from_hash(data, options)
57
+ end
58
+
59
+ def from_yaml(yaml, options = nil)
60
+ data = YAML.load_stream(StringIO.new(yaml)).documents
61
+ from_hash(data, options)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ module Ixtlan
2
+ module Babel
3
+ class Factory
4
+
5
+ class EmptyArraySerializer < Array
6
+ def use(arg)
7
+ self
8
+ end
9
+ end
10
+
11
+ def initialize(custom_serializers = {})
12
+ @map = {}
13
+ add('DateTime') do |dt|
14
+ dt.strftime('%Y-%m-%dT%H:%M:%S.') + ("%06d" % (dt.sec_fraction / Date::NANOSECONDS_IN_DAY / 1000)) + dt.strftime('%z')
15
+ end
16
+ add('ActiveSupport::TimeWithZone') do |tz|
17
+ tz.strftime('%Y-%m-%dT%H:%M:%S.') + ("%06d" % tz.usec) + tz.strftime('%z')
18
+ end
19
+ add('Time') do |t|
20
+ t.strftime('%Y-%m-%dT%H:%M:%S.') + ("%06d" % t.usec) + t.strftime('%z')
21
+ end
22
+ @map.merge!(custom_serializers)
23
+ end
24
+
25
+ def add(clazz, &block)
26
+ @map[clazz.to_s] = block
27
+ end
28
+
29
+ def new(resource)
30
+ if resource.respond_to?(:model)
31
+ model = resource.model
32
+ elsif resource.is_a? Array
33
+ if resource.empty?
34
+ return EmptyArraySerializer.new
35
+ else
36
+ r = resource.first
37
+ model = r.respond_to?(:model) ? r.model : r.class
38
+ end
39
+ else
40
+ model = resource.class
41
+ end
42
+ ser = Object.const_get("#{model}Serializer").new(resource)
43
+ ser.add_custom_serializers(@map)
44
+ ser
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,148 @@
1
+ module Ixtlan
2
+ module Babel
3
+ class HashFilter
4
+
5
+ def initialize(context_or_options = nil)
6
+ use(context_or_options)
7
+ end
8
+
9
+ private
10
+
11
+ def context
12
+ @context ||= {}
13
+ end
14
+
15
+ public
16
+
17
+ def add_custom_serializers(map)
18
+ @map = map
19
+ end
20
+
21
+ def default_context_key(default = nil)
22
+ @default = default if default
23
+ @default
24
+ end
25
+
26
+ def []=(key, options)
27
+ context[key.to_sym] = options if key
28
+ end
29
+
30
+ def [](key)
31
+ context[key.to_sym] if key
32
+ end
33
+
34
+ def use(context_or_options)
35
+ if context_or_options
36
+ case context_or_options
37
+ when Symbol
38
+ if opts = context[context_or_options]
39
+ @options = opts.dup
40
+ end
41
+ when Hash
42
+ @options = context_or_options
43
+ end
44
+ else
45
+ @options = nil
46
+ end
47
+ self
48
+ end
49
+
50
+ NO_MODEL = Object.new
51
+ def NO_MODEL.send(*args)
52
+ self
53
+ end
54
+ def NO_MODEL.[](*args)
55
+ self
56
+ end
57
+
58
+ def filter(hash = {}, model = NO_MODEL, &block)
59
+ filter_data(model, hash, options, &block) if hash
60
+ end
61
+
62
+ def options
63
+ @options || context[default_context_key] || {}
64
+ end
65
+
66
+ private
67
+
68
+ def filter_data(model, data, options = {}, &block)
69
+ model = NO_MODEL if model == data
70
+ only = options[:only].collect { |o| o.to_s } if options[:only]
71
+ except = (options[:except] || []).collect { |e| e.to_s }
72
+
73
+ include =
74
+ case options[:include]
75
+ when Array
76
+ options[:include].collect { |i| i.to_s }
77
+ when Hash
78
+ Hash[options[:include].collect {|k,v| [k.to_s, v]}]
79
+ else
80
+ []
81
+ end
82
+ if model != NO_MODEL
83
+ methods = (options[:methods] || []).collect { |e| e.to_s }
84
+ methods.each do |m|
85
+ data[m] = model.send(m.to_sym)
86
+ end
87
+
88
+ include_methods = include.is_a?(Array) ? include : include.keys
89
+ include_methods.each do |m|
90
+ unless data.include?(m)
91
+ raise "no block given to calculate the attributes from model" unless block
92
+ models_or_model = model.send(m)
93
+ if models_or_model.respond_to?(:collect)
94
+ data[m] = models_or_model.collect { |i| block.call(i) }
95
+ else
96
+ data[m]= block.call(model.send(m))
97
+ end
98
+ end
99
+ end
100
+ end
101
+ methods ||= []
102
+
103
+ result = {}
104
+ data.each do |k,v|
105
+ case v
106
+ when Hash
107
+ if include.include?(k.to_s)
108
+ case include
109
+ when Array
110
+ result[k.to_s] = filter_data(model.send(k), v, &block)
111
+ when Hash
112
+ result[k.to_s] = filter_data(model.send(k), v, include[k.to_s], &block)
113
+ end
114
+ end
115
+ when Array
116
+ if include.include?(k.to_s)
117
+ models = model.send(k)
118
+ j = -1
119
+ case include
120
+ when Array
121
+ result[k.to_s] = v.collect do |i|
122
+ j += 1
123
+ if i.is_a?(Array) || i.is_a?(Hash)
124
+ filter_data(models[j], i, &block)
125
+ else
126
+ i
127
+ end
128
+ end
129
+ when Hash
130
+ opts = include[k]
131
+ result[k.to_s] = v.collect { |i| j += 1; filter_data(models[j], i, opts, &block) }
132
+ end
133
+ end
134
+ else
135
+ if methods.include?(k.to_s) || (only && only.include?(k.to_s)) || (only.nil? && !except.include?(k.to_s))
136
+ if @map && ser = @map[v.class.to_s]
137
+ result[k.to_s] = ser.call(v)
138
+ else
139
+ result[k.to_s] = v
140
+ end
141
+ end
142
+ end
143
+ end
144
+ result
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,24 @@
1
+ require 'ixtlan/babel/serializer'
2
+ module Ixtlan
3
+ module Babel
4
+ class NoTimestampSerializer < Serializer
5
+
6
+ def self.add_defaults(root = nil)
7
+ if root
8
+ add_context(:default, :root => root)
9
+ add_no_timestamp_context(:collection, :root => root)
10
+ else
11
+ add_context(:default)
12
+ add_no_timestamp_context(:collection)
13
+ end
14
+ end
15
+
16
+ def self.add_no_timestamp_context(key, options = {})
17
+ except = (options[:except] || []).dup
18
+ except << :updated_at
19
+ except << :created_at
20
+ add_context(key, options.merge({:except => except}))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,115 @@
1
+ require 'ixtlan/babel/hash_filter'
2
+ module Ixtlan
3
+ module Babel
4
+ class Serializer
5
+
6
+ def id
7
+ if @model_or_models.is_a? Array
8
+ super
9
+ else
10
+ @model_or_models.id
11
+ end
12
+ end
13
+
14
+ def initialize(model_or_models)
15
+ @model_or_models = model_or_models
16
+ end
17
+
18
+ def respond_to?(method)
19
+ @model_or_models.respond_to?(method)
20
+ end
21
+
22
+ def method_missing(method, *args, &block)
23
+ @model_or_models.send(method, *args, &block)
24
+ end
25
+
26
+ def add_custom_serializers(map)
27
+ filter.add_custom_serializers(map)
28
+ end
29
+
30
+ private
31
+
32
+ def self.filter
33
+ @filter ||= HashFilter.new
34
+ end
35
+
36
+ def filter
37
+ @filter ||= self.class.filter.dup
38
+ end
39
+
40
+ protected
41
+
42
+ # for rails
43
+ def self.model(model = nil)
44
+ @model_class = model if model
45
+ @model_class ||= self.to_s.sub(/Serializer$/, '').constantize
46
+ end
47
+
48
+ # for rails
49
+ def self.model_name
50
+ model.model_name
51
+ end
52
+
53
+ def self.default_context_key(default)
54
+ filter.default_context_key(default)
55
+ end
56
+
57
+ def self.add_context(key, options = {})
58
+ filter[key] = options
59
+ end
60
+
61
+ public
62
+
63
+ def use(context_or_options)
64
+ filter.use(context_or_options)
65
+ self
66
+ end
67
+
68
+ def to_hash(options = nil)
69
+ filter.use(filter.options.dup.merge!(options)) if options
70
+ if @model_or_models.respond_to?(:collect) && ! @model_or_models.is_a?(Hash)
71
+ @model_or_models.collect do |m|
72
+ filter_model(attr(m), m)
73
+ end
74
+ else
75
+ filter_model(attr(@model_or_models), @model_or_models)
76
+ end
77
+ end
78
+
79
+ def to_json(options = nil)
80
+ to_hash(options).to_json
81
+ end
82
+
83
+ def to_xml(options = {})
84
+ opts = filter.options.dup.merge!(options)
85
+ root = opts.delete :root
86
+ fitler.use(opts)
87
+ result = to_hash
88
+ if root && result.is_a?(Array) && root.respond_to?(:pluralize)
89
+ root = root.to_s.pluralize
90
+ end
91
+ result.to_xml :root => root
92
+ end
93
+
94
+ def to_yaml(options = nil)
95
+ to_hash(options).to_yaml
96
+ end
97
+
98
+ protected
99
+
100
+ def attr(model)
101
+ model.attributes if model
102
+ end
103
+
104
+ private
105
+
106
+ def filter_model(model, data)
107
+ if root = filter.options[:root]
108
+ {root.to_s => filter.filter(model, data){ |model| attr(model) } }
109
+ else
110
+ filter.filter(model, data){ |model| attr(model) }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ class Hash
4
+ def attributes
5
+ self
6
+ end
7
+ def method_missing(method)
8
+ self[method.to_s]
9
+ end
10
+ end
11
+
12
+ describe Ixtlan::Babel do
13
+ let(:data) do
14
+ data = {
15
+ 'id' => 987,
16
+ 'name' => 'me and the corner',
17
+ 'address' => { 'street' => 'Foo 12', 'zipcode' => '12345' },
18
+ 'phone_numbers' => {
19
+ 'prefix' => 12,
20
+ 'number' => '123',
21
+ 'area' => { 'code' => '001', 'iso' => 'us'}
22
+ }
23
+ }
24
+ class Hash
25
+ def self.new(hash = nil, &block)
26
+ if hash
27
+ self[hash]
28
+ else
29
+ super &block
30
+ end
31
+ end
32
+ end
33
+ data
34
+ end
35
+
36
+ let(:serializer) { Ixtlan::Babel::Serializer.new(data) }
37
+ let(:deserializer) { Ixtlan::Babel::Deserializer.new(Hash) }
38
+
39
+ it 'should serialize and deserialize a hash' do
40
+ json = serializer.to_json
41
+ result = deserializer.from_json(json)
42
+ result.must_equal Hash['id' => data['id'], 'name' => data['name']]
43
+ end
44
+
45
+ it 'should serialize and deserialize a hash with root' do
46
+ json = serializer.to_json :root => :my
47
+ result = deserializer.from_json(json, :root => :my)
48
+ result.must_equal Hash['id' => data['id'], 'name' => data['name']]
49
+ end
50
+
51
+ it 'should serialize and deserialize a hash with include list' do
52
+ json = serializer.to_json(:include => ['address', 'phone_numbers'])
53
+ result = deserializer.from_json(json, :include => ['address', 'phone_numbers'])
54
+ data['phone_numbers'].delete('area')
55
+ result.must_equal Hash[data]
56
+ end
57
+
58
+ it 'should serialize and deserialize a hash with except' do
59
+ json = serializer.to_json(:except => ['id'])
60
+ result = deserializer.from_json(json, :except => ['id'])
61
+ result.must_equal Hash['name' => data['name']]
62
+ result = deserializer.from_json(json)
63
+ result.must_equal Hash['name' => data['name']]
64
+ end
65
+
66
+ it 'should serialize and deserialize a hash with only' do
67
+ json = serializer.to_json(:only => ['name'])
68
+ result = deserializer.from_json(json, :only => ['name'])
69
+ result.must_equal Hash['name' => data['name']]
70
+ result = deserializer.from_json(json)
71
+ result.must_equal Hash['name' => data['name']]
72
+ end
73
+
74
+ it 'should serialize and deserialize a hash with nested only' do
75
+ json = serializer.to_json(:include => { 'address' => {:only => ['street']}})
76
+ data.delete('phone_numbers')
77
+ data['address'].delete('zipcode')
78
+ result = deserializer.from_json(json, :include => { 'address' => {:only => ['street']}})
79
+ result.must_equal data
80
+ result = deserializer.from_json(json, :include => ['address'])
81
+ result.must_equal data
82
+ end
83
+
84
+ it 'should serialize and deserialize a hash with nested except' do
85
+ json = serializer.to_json(:include => { 'address' => {:except => ['zipcode']}})
86
+ data.delete('phone_numbers')
87
+ data['address'].delete('zipcode')
88
+ result = deserializer.from_json(json, :include => { 'address' => {:except => ['zipcode']}})
89
+ result.must_equal data
90
+ result = deserializer.from_json(json, :include => ['address'])
91
+ result.must_equal data
92
+ end
93
+
94
+ it 'should serialize and deserialize a hash with nested include' do
95
+ json = serializer.to_json(:include => { 'address' => {}, 'phone_numbers' => { :include => ['area']}})
96
+ result = deserializer.from_json(json, :include => { 'address' => {}, 'phone_numbers' => { :include => ['area']}})
97
+ result.must_equal data
98
+ end
99
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH << File.expand_path(File.join(__FILE__, '..', '..', 'lib'))
2
+ require 'minitest/autorun'
3
+ require 'json'
4
+ require 'ixtlan-babel'
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ixtlan-babel
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 2
10
+ version: 0.1.2
11
+ platform: ruby
12
+ authors:
13
+ - Kristian Meier
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-07-04 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - "="
27
+ - !ruby/object:Gem::Version
28
+ hash: 11
29
+ segments:
30
+ - 0
31
+ - 9
32
+ - 2
33
+ - 2
34
+ version: 0.9.2.2
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: json_pure
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 13
46
+ segments:
47
+ - 1
48
+ - 6
49
+ - 1
50
+ version: 1.6.1
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: minitest
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - "="
60
+ - !ruby/object:Gem::Version
61
+ hash: 37
62
+ segments:
63
+ - 2
64
+ - 11
65
+ - 3
66
+ version: 2.11.3
67
+ type: :development
68
+ version_requirements: *id003
69
+ description: babel offers a filter for hashes and with that comes json/yaml/xml de/serialization of models which provides a hash representationi. possible models are activerecord, activemodel, resources from datamapper, virtus
70
+ email:
71
+ - m.kristian@web.de
72
+ executables: []
73
+
74
+ extensions: []
75
+
76
+ extra_rdoc_files: []
77
+
78
+ files:
79
+ - lib/ixtlan-babel.rb
80
+ - lib/ixtlan/babel/deserializer.rb
81
+ - lib/ixtlan/babel/hash_filter.rb
82
+ - lib/ixtlan/babel/no_timestamp_serializer.rb
83
+ - lib/ixtlan/babel/factory.rb
84
+ - lib/ixtlan/babel/serializer.rb
85
+ - spec/spec_helper.rb
86
+ - spec/filter_spec.rb
87
+ - MIT-LICENSE
88
+ - README.md
89
+ - Gemfile
90
+ - Gemfile.lock
91
+ homepage: https://github.com/mkristian/ixtlan-babel
92
+ licenses: []
93
+
94
+ post_install_message:
95
+ rdoc_options: []
96
+
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ requirements: []
118
+
119
+ rubyforge_project:
120
+ rubygems_version: 1.8.21
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: babel offers a filter for hashes and with that comes json/yaml/xml de/serialization of models which provides a hash representation
124
+ test_files:
125
+ - spec/filter_spec.rb