candywrapper 0.0.1

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.
@@ -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