candywrapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ *.swp
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in candywrapper.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Steve Jang
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,173 @@
1
+ # Candywrapper
2
+
3
+ This gem provides simple wrapper around a regular ruby Hash object.
4
+ This wrapper provides the following functionality:
5
+ - DSL for defining arbitrarily deep serializable structure
6
+ - JSON serialization
7
+
8
+ This gem is useful when you want to serialize/deserialize Ruby
9
+ objects to store or to transmit them over network. This gem was designed
10
+ with the following advantages in mind:
11
+
12
+ 1. You can mix-in the functionality to existing Ruby objects.
13
+
14
+ 2. DSL explicitly defines the protocol payload
15
+ - Serialization only includes whatever is defined via DSL.
16
+ All other object states are ignored. This clearly delineates
17
+ object states that are transient from those that are to be
18
+ preserved across serialization.
19
+ - Attributes defined via Candywrapper DSL are called
20
+ "serializable attributes".
21
+
22
+ 3. Light-weight and fast
23
+ - Serialization payload is represented by a single hash object
24
+ (containing nested hashes). There is no other bookkeeping
25
+ going on other than that.
26
+ - (De-)Serialization is fast since we just use C-compiled JSON
27
+ gem of the 1 hash object. We never recursively traverse the
28
+ nested hash structure, since doing that in Ruby would be
29
+ 50-70 times slower than in C.
30
+
31
+ 4. You can nest candywrappers inside candywrappers.
32
+ - If you nest candywrapper object 2 and object 3 inside
33
+ candywrapper object 1, there is still only 1 nested hash
34
+ object wrapped by object 1. In this case, the hash object
35
+ used by objects 2 and 3 are nested inside the hash object
36
+ used by object 1. This means that we only ever pass 1 hash
37
+ object to json generator during serialization.
38
+ - What happens to object 2 and object 3 in the above story?
39
+ Those objects are just wrappers around the nested (sub-)
40
+ hashes of object 1's hash. All these relationships / references
41
+ are correctly maintained within the accessor methods.
42
+
43
+ Given this design, please be aware that:
44
+
45
+ 1. Any object state that you want to save or send should be
46
+ stored in the wrapped hash object. (see usage)
47
+
48
+ 2. The only types that can be stored as serializable attribute
49
+ are basic JSON primitives, or other candywrapper objects.
50
+ (see usage) For example, you cannot store a File object in
51
+ a candywrapper attribute.
52
+
53
+ 3. We do not support multiple references to the same object, circular or not.
54
+ If you assign the same complex object across multiple serializable
55
+ attributes, the behavior is undefined. (depending on how JSON generator
56
+ implementation behaves)
57
+
58
+ 4. Cloning a candywrapper object (.clone) will perform deep copy.
59
+ If we don't do this, you could end up with two separate objects
60
+ sharing the same internal state.
61
+
62
+ 5. All serializable attributes are optional in nature. If you don't set
63
+ them, they won't be part of the serialized payload.
64
+
65
+ ## Known Issues / Todo
66
+
67
+ 1. We do not provide validation of values being assigned to serializable
68
+ attributes. The reason for this is to keep things as minimal as possible.
69
+ If we were to provide validation, we would also have to go through
70
+ any nested raw hash object to see if they satisfy the structure specified
71
+ in DSL, which means recursively traversing all the elements of the raw
72
+ hash during deserialization.
73
+
74
+ The only thing that we check for you is whether something is allowed to
75
+ be nil or not, as such feature tends to catch a lot of real bugs.
76
+
77
+ 2. Currently, we do not support nested array of candywrappers. For now,
78
+ arrays must only contain other JSON primitives.
79
+ When we support this in the future, we will probably require each
80
+ array explicitly define the element type, and we will not allow mixing
81
+ of multiple candywrapper types within the same array.
82
+
83
+ 3. Currently, we do not support arbitrary hash containing candywrappers
84
+ as values. The reason for this is that to support deserializing a
85
+ candywrapper object from value within an arbitrarily nested hash
86
+ object requires decorating the raw payload. (e.g. mark a hash object
87
+ as being payload of a candywrapper class using '_class_name' key)
88
+
89
+ Related advice here is that you stick to explicitly defining your object
90
+ structure, rather than leaving it up to some run-time interpretation
91
+ (based on existence of a specific key-val). I've tried to support this
92
+ kind of functionality before in a previous job, and it became pretty ugly.
93
+
94
+
95
+ ## Installation
96
+
97
+ Add this line to your application's Gemfile:
98
+
99
+ gem 'candywrapper'
100
+
101
+ And then execute:
102
+
103
+ $ bundle
104
+
105
+ Or install it yourself as:
106
+
107
+ $ gem install candywrapper
108
+
109
+ ## Usage
110
+
111
+ Here is a simple example to illustrate how to use Candywrapper.
112
+
113
+ require 'candywrapper'
114
+
115
+ class Address
116
+ include Candywrapper
117
+ serializable_attr :street
118
+ serializable_attr :city
119
+ serializable_attr :state
120
+ serializable_attr :zip
121
+ end
122
+
123
+ class Person
124
+ include Candywrapper
125
+ serializable_attr :first_name
126
+ serializable_attr :middle_name
127
+ serializable_attr :last_name
128
+ serializable_attr :home_address, Address
129
+ serializable_attr :work_address, Address
130
+ def full_name
131
+ @full_name = [first_name, middle_name, last_name].compact.join(" ")
132
+ end
133
+ end
134
+
135
+ a = Address.new
136
+ a.street = "120 Cherry ST N"
137
+ a.city = "Seattle"
138
+ a.state = "WA"
139
+ a.zip = "98101"
140
+
141
+ p = Person.new
142
+ p.home_address = a
143
+ p.first_name = "James"
144
+ p.last_name = "Bond"
145
+ p.full_name # => "James Bond"
146
+
147
+ json = p.serialize_to_json
148
+ # =>
149
+ # {
150
+ # "first_name": "James",
151
+ # "last_name": "Bond",
152
+ # "home_address": {
153
+ # "street": "120 Cherry ST N",
154
+ # "city": "Seattle",
155
+ # "state": "WA",
156
+ # "zip": "98101"
157
+ # }
158
+ # }
159
+
160
+ p2 = Person.deserialize_from_json(json)
161
+ p2.class # => Person
162
+ p2.home_address.class # => Address
163
+ p2.work_address # => nil
164
+ p2.full_name # => "James Bond"
165
+
166
+
167
+ ## Contributing
168
+
169
+ 1. Fork it
170
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
171
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
172
+ 4. Push to the branch (`git push origin my-new-feature`)
173
+ 5. Create new Pull Request
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rake"
4
+ require "rake/testtask"
5
+
6
+ namespace :test do
7
+
8
+ Rake::TestTask.new(:unit) do |test|
9
+ test.libs << 'test'
10
+ test.pattern = '{test/unit/**/*_test.rb}'
11
+ end
12
+
13
+ Rake::TestTask.new(:all) do |test|
14
+ test.libs << 'test'
15
+ test.pattern = '{test/**/*_test.rb}'
16
+ end
17
+
18
+ end
19
+
20
+ task :test => "test:unit"
21
+ task :default => "test"
22
+
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/candywrapper/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Steve Jang"]
6
+ gem.email = ["estebanjang@gmail.com"]
7
+ gem.description = %q{This gem provides DSL for specifying attributes to be included in serailization.}
8
+ gem.summary = %q{Please see README.md file for full description of what this gem does.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "candywrapper"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Candywrapper::VERSION
17
+
18
+ gem.add_dependency("json")
19
+ end
@@ -0,0 +1,136 @@
1
+ require "time"
2
+ require "json"
3
+ require "candywrapper/version"
4
+
5
+ # === Description
6
+ # Mix-in to add serializable attribute support to a class.
7
+ # Please refer to candywrapper gem README.md documentation.
8
+ #
9
+ module Candywrapper
10
+
11
+ CANDYWRAPPER_OBJ = '@candywrapper'.freeze
12
+
13
+ def payload_hash
14
+ @payload_hash ||= {}
15
+ end
16
+
17
+ def payload_hash_set(h)
18
+ if h.is_a?(Hash)
19
+ @payload_hash = h
20
+ h.instance_variable_set(CANDYWRAPPER_OBJ, self)
21
+ else
22
+ raise "payload_hash cannot be a non-hash object: #{h.inspect}"
23
+ end
24
+ end
25
+
26
+ def serialize_to_json
27
+ JSON.generate(@payload_hash)
28
+ end
29
+
30
+ private
31
+
32
+ def candywrapper_object_get(name_s, type, payload_val)
33
+ obj = payload_val.instance_variable_get(CANDYWRAPPER_OBJ)
34
+ unless obj
35
+ obj = type.candywrap_hash(payload_val)
36
+ payload_val.instance_variable_set(CANDYWRAPPER_OBJ, obj)
37
+ end
38
+ obj
39
+ end
40
+
41
+ def candywrapper_object_set(name_s, type, val)
42
+ if val.is_a?(type)
43
+ payload_hash[name_s] = val.payload_hash
44
+ else
45
+ raise "Expected #{type.inspect} but got #{val.inspect} for #{name_s}"
46
+ end
47
+ end
48
+
49
+ def candywrapper_attr_get(name_s, type, conversion, opt = {})
50
+
51
+ payload_val = payload_hash[name_s]
52
+ if payload_val.nil?
53
+ conversion = :none # forgive non-existent attribute in payload
54
+ end
55
+
56
+ case conversion
57
+ when :none
58
+ return payload_val
59
+ when :candywrapper
60
+ return candywrapper_object_get(name_s, type, payload_val)
61
+ when :iso8601
62
+ return Time.parse(payload_val)
63
+ end
64
+ end
65
+
66
+ def candywrapper_attr_set(name_s, type, conversion, val, opt = {})
67
+
68
+ if val.nil?
69
+ raise "Nil is not allowed for #{name_s} attribute" if opt[:no_nil]
70
+ conversion = :none
71
+ end
72
+
73
+ case conversion
74
+ when :none
75
+ payload_hash[name_s] = val
76
+ when :candywrapper
77
+ candywrapper_object_set(name_s, type, val)
78
+ when :iso8601
79
+ if val.is_a?(Time)
80
+ payload_hash[name_s] = val.utc.strftime("%FT%T%z")
81
+ else
82
+ raise "Expected #{type.inspect} but got #{val.inspect} for #{name_s}"
83
+ end
84
+ end
85
+ end
86
+
87
+ module ClassMethods
88
+
89
+ def candywrap_hash(h)
90
+ obj = self.new
91
+ obj.payload_hash_set(h)
92
+ obj
93
+ end
94
+
95
+ def deserialize_from_json(json)
96
+ h = JSON.parse(json)
97
+ candywrap_hash(h)
98
+ end
99
+
100
+ # === Description
101
+ # Add a serializable attribute to the current class.
102
+ #
103
+ # === Parameters
104
+ # name:: (Symbol) Attribute name
105
+ # type:: (Class) Class of the attribute value; nil to indicate "don't care"
106
+ # opt:: (Hash) Additional options
107
+ # * opt[:no_nil]:: do not allow nil assignment
108
+ #
109
+ def serializable_attr(name, type = nil, opt = {})
110
+
111
+ name_s = name.to_s
112
+ if type.respond_to?(:ancestors) and type.ancestors.include?(Candywrapper)
113
+ conversion = :candywrapper
114
+ elsif type == Time
115
+ conversion = :iso8601
116
+ else
117
+ conversion = :none
118
+ end
119
+
120
+ define_method name_s do
121
+ candywrapper_attr_get(name_s, type, conversion, opt)
122
+ end
123
+
124
+ define_method "#{name_s}=" do |val|
125
+ candywrapper_attr_set(name_s, type, conversion, val, opt)
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+ def self.included(c)
132
+ class << c
133
+ include ClassMethods
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,3 @@
1
+ module Candywrapper
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,165 @@
1
+ ENV['SERVICE_ROOT'] = File.expand_path(File.dirname(__FILE__))
2
+ require 'minitest/autorun'
3
+ require 'candywrapper'
4
+
5
+ class Address
6
+ include Candywrapper
7
+ serializable_attr :street
8
+ serializable_attr :city
9
+ serializable_attr :state
10
+ serializable_attr :zip
11
+ serializable_attr :created_at, Time
12
+ end
13
+
14
+ class Person
15
+ include Candywrapper
16
+ serializable_attr :first_name
17
+ serializable_attr :middle_name
18
+ serializable_attr :last_name
19
+ serializable_attr :home_address, Address
20
+ serializable_attr :work_address, Address
21
+ serializable_attr :created_at, Time
22
+ serializable_attr :database_id, Integer, :no_nil => true
23
+ def full_name
24
+ @full_name = [first_name, middle_name, last_name].compact.join(" ")
25
+ end
26
+ end
27
+
28
+ class CandywrapperTest < MiniTest::Unit::TestCase
29
+
30
+ def setup
31
+ @a = Address.new
32
+ @a.street = "120 Cherry ST N"
33
+ @a.city = "Seattle"
34
+ @a.state = "WA"
35
+ @a.zip = "98101"
36
+
37
+ @p = Person.new
38
+ @p.home_address = @a
39
+ @p.first_name = "James"
40
+ @p.last_name = "Bond"
41
+ end
42
+
43
+ def test_accessors
44
+
45
+ assert_equal(@p.home_address.class, Address)
46
+ assert_equal("120 Cherry ST N", @p.home_address.street)
47
+ assert_equal("Seattle", @p.home_address.city)
48
+ assert_equal("WA", @p.home_address.state)
49
+ assert_equal("98101", @p.home_address.zip)
50
+ assert_equal(nil, @p.work_address)
51
+
52
+ ah = @p.home_address.payload_hash
53
+ assert_equal("120 Cherry ST N", ah['street'])
54
+ assert_equal("Seattle", ah['city'])
55
+ assert_equal("WA", ah['state'])
56
+ assert_equal("98101", ah['zip'])
57
+
58
+ assert_equal("James", @p.first_name)
59
+ assert_equal("Bond", @p.last_name)
60
+ assert_equal(nil, @p.middle_name)
61
+ assert_equal("James Bond", @p.full_name)
62
+
63
+ end
64
+
65
+ def test_time_conversion
66
+
67
+ t1 = Time.parse('2012-08-18T06:10:00 MDT')
68
+ t2 = Time.parse('2012-08-18T06:20:00 MDT')
69
+ t3 = Time.parse('2012-08-18T05:20:00 PDT')
70
+ assert_equal(t2, t3)
71
+
72
+ @p.created_at = t1
73
+ @p.home_address.created_at = t2
74
+
75
+ assert_equal("2012-08-18T12:10:00+0000", @p.payload_hash['created_at'])
76
+ assert_equal("2012-08-18T12:20:00+0000", @p.payload_hash['home_address']['created_at'])
77
+
78
+ json = @p.serialize_to_json
79
+
80
+ p2 = Person.deserialize_from_json(json)
81
+ assert_equal("2012-08-18T12:10:00+0000", p2.payload_hash['created_at'])
82
+ assert_equal("2012-08-18T12:20:00+0000", p2.payload_hash['home_address']['created_at'])
83
+
84
+ assert_equal(t1, p2.created_at)
85
+ assert_equal(t2, p2.home_address.created_at)
86
+ assert_equal(t3, p2.home_address.created_at)
87
+
88
+ end
89
+
90
+ def test_serialization
91
+
92
+ json = @p.serialize_to_json
93
+
94
+ h = JSON.parse(json)
95
+ assert_equal('James', h['first_name'])
96
+ assert_equal('Bond', h['last_name'])
97
+ assert_equal(nil, h['middle_name'])
98
+ assert_equal('120 Cherry ST N', h['home_address']['street'])
99
+ assert_equal('Seattle', h['home_address']['city'])
100
+ assert_equal('WA', h['home_address']['state'])
101
+ assert_equal('98101', h['home_address']['zip'])
102
+
103
+ end
104
+
105
+ def test_deserialization
106
+
107
+ json = @p.serialize_to_json
108
+
109
+ p2 = Person.deserialize_from_json(json)
110
+ assert_equal("James", p2.first_name)
111
+ assert_equal(nil, p2.middle_name)
112
+ assert_equal("Bond", p2.last_name)
113
+ assert_equal(nil, p2.work_address)
114
+ assert_equal(nil, p2.created_at)
115
+ assert_equal("James Bond", p2.full_name)
116
+
117
+ assert_equal("120 Cherry ST N", p2.home_address.street)
118
+ assert_equal("Seattle", p2.home_address.city)
119
+ assert_equal("WA", p2.home_address.state)
120
+ assert_equal("98101", p2.home_address.zip)
121
+ assert_equal(nil, p2.home_address.created_at)
122
+
123
+ end
124
+
125
+ def test_errors
126
+
127
+ begin
128
+ @p.database_id = nil
129
+ rescue => e
130
+ assert(e.to_s =~ /Nil is not allowed/)
131
+ end
132
+
133
+ begin
134
+ @p.work_address = "100 5th Ave"
135
+ rescue => e
136
+ assert(e.to_s =~ /Expected Address but got/)
137
+ end
138
+
139
+ begin
140
+ @p.created_at = "2012-01-01"
141
+ rescue => e
142
+ assert(e.to_s =~ /Expected Time but got/)
143
+ end
144
+
145
+ # Bad type in payload is forgiven, until you try to access it
146
+ #
147
+ h = JSON.parse(@p.serialize_to_json)
148
+ h['work_address'] = "100 5th Ave"
149
+ json = JSON.generate(h)
150
+ p2 = Person.deserialize_from_json(json)
151
+ begin
152
+ p2.work_address
153
+ rescue => e
154
+ assert(e.to_s =~ /payload_hash cannot be a non-hash object/)
155
+ end
156
+
157
+ # No exceptions
158
+ #
159
+ p2.work_address = Address.new # we can fix it later
160
+ p2.work_address.street = "100 5th Ave"
161
+
162
+ end
163
+
164
+ end
165
+
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: candywrapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Jang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &11759500 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *11759500
25
+ description: This gem provides DSL for specifying attributes to be included in serailization.
26
+ email:
27
+ - estebanjang@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - candywrapper.gemspec
38
+ - lib/candywrapper.rb
39
+ - lib/candywrapper/version.rb
40
+ - test/unit/basic_test.rb
41
+ homepage: ''
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.11
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Please see README.md file for full description of what this gem does.
65
+ test_files:
66
+ - test/unit/basic_test.rb