ixtlan-babel 0.1.2

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