rep 0.0.1 → 0.0.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.
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html;charset=utf-8">
5
+ <title>version.rb</title>
6
+ <link rel="stylesheet" href="http://jashkenas.github.com/docco/resources/docco.css">
7
+ </head>
8
+ <body>
9
+ <div id='container'>
10
+ <div id="background"></div>
11
+ <div id="jump_to">
12
+ Jump To &hellip;
13
+ <div id="jump_wrapper">
14
+ <div id="jump_page">
15
+ <a class="source" href="../rep.html">rep.rb</a>
16
+ <a class="source" href="version.html">version.rb</a>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ <table cellspacing=0 cellpadding=0>
21
+ <thead>
22
+ <tr>
23
+ <th class=docs><h1>version.rb</h1></th>
24
+ <th class=code></th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <tr id='section-1'>
29
+ <td class=docs>
30
+ <div class="pilwrap">
31
+ <a class="pilcrow" href="#section-1">&#182;</a>
32
+ </div>
33
+
34
+
35
+ </td>
36
+ <td class=code>
37
+ <div class='highlight'><pre><span class="k">module</span> <span class="nn">Rep</span>
38
+ <span class="no">VERSION</span> <span class="o">=</span> <span class="s2">&quot;0.0.1&quot;</span>
39
+ <span class="k">end</span></pre></div>
40
+ </td>
41
+ </tr>
42
+ </table>
43
+ </div>
44
+ </body>
data/lib/rep.rb CHANGED
@@ -1,13 +1,51 @@
1
- require "rep/version"
1
+ # **Rep** is a small module to endow any class to make json quickly. It solves four problems:
2
+ #
3
+ # 1. Enumerating top level keys for a json structure
4
+ # 2. Providing a convention for the value of those keys
5
+ # 3. Defining `attr_accessor`'s that are prefilled from an options hash given to `#initialize`
6
+ # 4. Sharing instances to help GC
7
+ #
8
+ # The code is available on [github](http://github.com/myobie/rep).
9
+
10
+ # `Forwardable` is in the stdlib and allows ruby objects to delegate methods off to other objects. An example:
11
+ #
12
+ # class A
13
+ # extend Forwardable
14
+ # delegate [:length, :first] => :@array
15
+ # def initialize(array = [])
16
+ # @array = array
17
+ # end
18
+ # end
19
+ #
20
+ # A.new([1,2,3]).length # => 3
21
+ # A.new([1,2,3]).first # => 1
22
+
2
23
  require 'forwardable'
24
+
25
+ # `JSON::generate` and `JSON::decode` are much safer to use than `Object#to_json`.
26
+
3
27
  require 'json'
4
28
 
29
+ require 'rep/version'
5
30
  module Rep
31
+
32
+ # All classes that `include Rep` are extended with `Forwardable`,
33
+ # given some aliases, endowned with `HashieSupport` if Hashie is loaded,
34
+ # and given a delegate method if it doesn't already have one.
35
+
6
36
  def self.included(klass)
7
37
  klass.extend Forwardable
8
38
  klass.extend ClassMethods
9
39
  klass.instance_eval {
10
40
  class << self
41
+ unless defined?(delegate)
42
+ def delegate(opts = {})
43
+ methods, object_name = opts.to_a.first
44
+ args = [object_name, methods].flatten
45
+ def_delegators *args
46
+ end
47
+ end
48
+
11
49
  alias forward delegate
12
50
 
13
51
  unless defined?(fields)
@@ -18,21 +56,29 @@ module Rep
18
56
  include HashieSupport
19
57
  end
20
58
  end
21
-
22
- unless defined?(parse_opts)
23
- def parse_opts(opts = {})
24
- # NOOP
25
- end
26
- end
27
59
  }
28
60
  end
29
61
 
62
+ # Since a goal is to be able to share instances, we need an easy way to reset a
63
+ # shared instance back to factory defaults. If you memoize any methods that are
64
+ # not declared as json fields, then overried this method and set any memoized
65
+ # variables to nil, then super.
66
+
30
67
  def reset_for_json!
31
68
  self.class.all_json_methods.each do |method_name|
32
69
  instance_variable_set(:"@#{method_name}", nil)
33
70
  end
34
71
  end
35
72
 
73
+ # All the work of generating a hash from an instance is packaged up in one method. Since
74
+ # fields can be aliases in the format `{ :json_key_name => :method_name }`, there
75
+ # is some fancy logic to determine the `field_name` and `method_name` variables.
76
+ #
77
+ # { :one => :foo }.to_a # => [[:one, :foo]]
78
+ #
79
+ # Right now it will raise if either a field doesn't have a method to provide it's value or
80
+ # if there are no json fields setup for the particular set (which defaults to `:default`).
81
+
36
82
  def to_hash(name = :default)
37
83
  if fields = self.class.json_fields(name)
38
84
  fields.reduce({}) do |memo, field|
@@ -55,6 +101,15 @@ module Rep
55
101
  end
56
102
 
57
103
  module ClassMethods
104
+
105
+ # Defines an attr_accessor with a default value. The default for default is nil. Example:
106
+ #
107
+ # class A
108
+ # register_accessor :name => "No Name"
109
+ # end
110
+ #
111
+ # A.new.name # => "No Name"
112
+
58
113
  def register_accessor(acc)
59
114
  name, default = acc.is_a?(Hash) ? acc.to_a.first : [acc, nil]
60
115
  attr_accessor name
@@ -66,19 +121,38 @@ module Rep
66
121
  end
67
122
  end
68
123
 
124
+ # Defines an `#initialize` method that accepts a Hash argument and copies some keys out into `attr_accessors`.
125
+ # If your class already has an `#iniatialize` method then this will overwrite it (so don't use it). `#initialize_with`
126
+ # does not have to be used to use any other parts of Rep.
127
+
69
128
  def initialize_with(*args)
70
129
  @initializiation_args = args
71
130
 
72
- define_singleton_method :initializiation_args do
73
- @initializiation_args
131
+ # Remember what args we normally initialize with so we can refer to them when building shared instances.
132
+
133
+ if defined?(define_singleton_method)
134
+ define_singleton_method :initializiation_args do
135
+ @initializiation_args
136
+ end
137
+ else
138
+ singleton = class << self; self end
139
+ singleton.send :define_method, :initializiation_args, lambda { @initializiation_args }
74
140
  end
75
141
 
142
+ # Create an `attr_accessor` for each one. Defaults can be provided using the Hash version { :arg => :default_value }
143
+
76
144
  args.each { |a| register_accessor(a) }
77
145
 
78
- define_method(:initialize) { |opts = {}| parse_opts(opts) }
146
+ define_method(:initialize) { |*args|
147
+ opts = args.first || {}
148
+ parse_opts(opts)
149
+ }
150
+
151
+ # `#parse_opts` is responsable for getting the `attr_accessor` values prefilled. Since defaults can be specified, it
152
+ # must negotiate Hashes and use the first key of the hash for the `attr_accessor`'s name.
79
153
 
80
154
  define_method :parse_opts do |opts|
81
- @presidential_options = opts
155
+ @rep_options = opts
82
156
  self.class.initializiation_args.each do |field|
83
157
  name = field.is_a?(Hash) ? field.to_a.first.first : field
84
158
  instance_variable_set(:"@#{name}", opts[name])
@@ -86,6 +160,32 @@ module Rep
86
160
  end
87
161
  end
88
162
 
163
+ # `#json_fields` setups up some class instance variables to remember sets of top level keys for json structures. Example:
164
+ #
165
+ # class A
166
+ # json_fields [:one, :two, :three] => :default
167
+ # end
168
+ #
169
+ # A.json_fields(:default) # => [:one, :two, :three]
170
+ #
171
+ # There is a general assumption that each top level key's value is provided by a method of the same name on an instance
172
+ # of the class. If this is not true, a Hash syntax can be used to alias to a different method name. Example:
173
+ #
174
+ # class A
175
+ # json_fields [{ :one => :the_real_one_method }, :two, { :three => :some_other_three }] => :default
176
+ # end
177
+ #
178
+ # Once can also set multiple sets of fields. Example:
179
+ #
180
+ # class A
181
+ # json_fields [:one, :two, :three] => :default
182
+ # json_fields [:five, :two, :six] => :other
183
+ # end
184
+ #
185
+ # And all fields are returned by calling `#json_fields` with no args. Example:
186
+ #
187
+ # A.json_fields # => { :default => [:one, :two, :three], :other => [:five, :two, :six] }
188
+
89
189
  def json_fields(arg = nil)
90
190
  if arg.is_a?(Hash)
91
191
  fields, name = arg.to_a.first
@@ -102,6 +202,9 @@ module Rep
102
202
  end
103
203
  end
104
204
 
205
+ # `#flat_json_fields` is just a utility method to DRY up the next two methods, because their code is almost exactly the same,
206
+ # it is not intended for use directly and might be confusing.
207
+
105
208
  def flat_json_fields(side = :right)
106
209
  side_number = side == :right ? 1 : 0
107
210
 
@@ -116,22 +219,83 @@ module Rep
116
219
  end.uniq
117
220
  end
118
221
 
222
+ # We need a way to get a flat, uniq'ed list of all the fields accross all field sets. This is that.
223
+
119
224
  def all_json_fields
120
225
  flat_json_fields(:left)
121
226
  end
122
227
 
228
+ # We need a wya to get a flat, uniq'ed list of all the method names accross all field sets. This is that.
229
+
123
230
  def all_json_methods
124
231
  flat_json_fields(:right)
125
232
  end
126
233
 
127
- # TODO: thread safety
234
+ # An easy way to save on GC is to use the same instance to turn an array of objects into hashes instead
235
+ # of instantiating a new object for every object in the array. Here is an example of it's usage:
236
+ #
237
+ # class BookRep
238
+ # initialize_with :book_model
239
+ # fields :title => :default
240
+ # forward :title => :book_model
241
+ # end
242
+ #
243
+ # BookRep.shared(:book_model => Book.first).to_hash # => { :title => "Moby Dick" }
244
+ # BookRep.shared(:book_model => Book.last).to_hash # => { :title => "Lost Horizon" }
245
+ #
246
+ # This should terrify you. If it doesn't, then this example will:
247
+ #
248
+ # book1 = BookRep.shared(:book_model => Book.first)
249
+ # book2 = BookRep.shared(:book_model => Book.last)
250
+ #
251
+ # boo1.object_id === book2.object_id # => true
252
+ #
253
+ # **It really is a shared object.**
254
+ #
255
+ # You really shouldn't use this method directly for anything.
256
+
128
257
  def shared(opts = {})
129
- @instance ||= new
130
- @instance.reset_for_json!
131
- @instance.parse_opts(opts)
132
- @instance
258
+ @pointer = (Thread.current[:rep_shared_instances] ||= {})
259
+ @pointer[object_id] ||= new
260
+ @pointer[object_id].reset_for_json!
261
+ @pointer[object_id].parse_opts(opts)
262
+ @pointer[object_id]
133
263
  end
134
264
 
265
+ # The fanciest thing in this entire library is this `#to_proc` method. Here is an example of it's usage:
266
+ #
267
+ # class BookRep
268
+ # initialize_with :book_model
269
+ # fields :title => :default
270
+ # forward :title => :book_model
271
+ # end
272
+ #
273
+ # Book.all.map(&BookRep) # => [{ :title => "Moby Dick" }, { :title => "Lost Horizon " }]
274
+ #
275
+ # And now I will explain how it works. Any object can have a to_proc method and when you call `#map` on an
276
+ # array and hand it a proc it will in turn hand each object as an argument to that proc. What I've decided
277
+ # to do with this object is use it the options for a shared instance to make a hash.
278
+ #
279
+ # Since I know the different initialization argumants from a call to `initialize_with`, I can infer by order
280
+ # which object is which option. Then I can create a Hash to give to `parse_opts` through the `shared` method.
281
+ # I hope that makes sense.
282
+ #
283
+ # It allows for extremely clean Rails controllers like this:
284
+ #
285
+ # class PhotosController < ApplicationController
286
+ # respond_to :json, :html
287
+ #
288
+ # def index
289
+ # @photos = Photo.paginate(page: params[:page], per_page: 20)
290
+ # respond_with @photos.map(&PhotoRep)
291
+ # end
292
+ #
293
+ # def show
294
+ # @photo = Photo.find(params[:id])
295
+ # respond_with PhotoRep.new(photo: @photo)
296
+ # end
297
+ # end
298
+
135
299
  def to_proc
136
300
  proc { |obj|
137
301
  arr = [obj].flatten
@@ -1,3 +1,3 @@
1
1
  module Rep
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -17,4 +17,8 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
  gem.add_dependency 'json'
20
+ gem.add_development_dependency 'rake'
21
+ gem.add_development_dependency 'minitest'
22
+ gem.add_development_dependency 'rocco'
23
+ gem.add_development_dependency 'rdiscount'
20
24
  end
@@ -23,10 +23,6 @@ describe Rep do
23
23
  new_rep_class.must_respond_to :fields
24
24
  end
25
25
 
26
- it "has a parse_opts method" do
27
- new_rep_class.must_respond_to :parse_opts
28
- end
29
-
30
26
  it "can have fields" do
31
27
  klass = new_rep_class do
32
28
  fields [:foo, :bar] => :default
@@ -38,24 +34,24 @@ describe Rep do
38
34
  klass = new_rep_class do
39
35
  initialize_with :foo, :bar
40
36
  end
41
- inst = klass.new(foo: 'foo123')
37
+ inst = klass.new(:foo => 'foo123')
42
38
  inst.foo.must_equal 'foo123'
43
39
  inst.bar.must_be_nil
44
40
  end
45
41
 
46
42
  it "can have default initialization options" do
47
43
  klass = new_rep_class do
48
- initialize_with :foo, { bar: "barbar" }
44
+ initialize_with :foo, { :bar => "barbar" }
49
45
  end
50
- inst = klass.new(foo: 'foofoo')
46
+ inst = klass.new(:foo => 'foofoo')
51
47
  inst.bar.must_equal 'barbar'
52
48
  end
53
49
 
54
50
  it "can overried default initialization options" do
55
51
  klass = new_rep_class do
56
- initialize_with :foo, { bar: "barbar" }
52
+ initialize_with :foo, { :bar => "barbar" }
57
53
  end
58
- inst = klass.new(bar: 'notbar')
54
+ inst = klass.new(:bar => 'notbar')
59
55
  inst.bar.must_equal 'notbar'
60
56
  inst.foo.must_be_nil
61
57
  end
@@ -84,7 +80,7 @@ describe Rep do
84
80
  def two; 2; end
85
81
  def three; 3; end
86
82
  end
87
- klass.new.to_hash.must_equal one: 1, two: 2, three: 3
83
+ klass.new.to_hash.must_equal :one => 1, :two => 2, :three => 3
88
84
  end
89
85
 
90
86
  it "should send fields to instance to make json" do
@@ -133,10 +129,48 @@ describe Rep do
133
129
  fields :keys => :default
134
130
  forward :keys => :hash
135
131
  end
136
- hashes = [{ one: 1, two: 2 },
137
- { three: 3, four: 4 },
138
- { one: 1, five: 5 }]
132
+ hashes = [{ :one => 1, :two => 2 },
133
+ { :three => 3, :four => 4 },
134
+ { :one => 1, :five => 5 }]
139
135
  hashes.map(&klass).to_json.must_equal '[{"keys":["one","two"]},{"keys":["three","four"]},{"keys":["one","five"]}]'
140
136
  end
141
137
 
138
+ describe "shared" do
139
+
140
+ User = Struct.new(:name, :age)
141
+ def users
142
+ @users ||= %w(Nathan 28 Jason 31 Justin 23).each_slice(2).
143
+ map { |name, age| User.new(name, age.to_i) }
144
+ end
145
+ class UserRep
146
+ include Rep
147
+ initialize_with :user
148
+ fields [:name, :age, :random_number] => :default
149
+ forward fields(:default) => :user
150
+
151
+ def random_number
152
+ @random_number ||= rand(100)
153
+ end
154
+ end
155
+
156
+ it "should memoize random_number" do
157
+ rep = UserRep.new(:user => users.first)
158
+ rep.random_number.must_equal rep.random_number
159
+ end
160
+
161
+ it "should get a clean instance each time" do
162
+ num1 = UserRep.shared(:user => users.first).random_number
163
+ num2 = UserRep.shared(:user => users.first).random_number
164
+ num1.wont_equal num2
165
+ end
166
+
167
+ it "should be really really shared" do
168
+ rep1 = UserRep.shared(:user => users.first)
169
+ rep2 = UserRep.shared(:user => users.last)
170
+ rep1.must_equal rep2
171
+ rep1.name.must_equal rep2.name
172
+ end
173
+
174
+ end
175
+
142
176
  end
@@ -1,9 +1,3 @@
1
1
  require 'minitest/autorun'
2
2
  require 'minitest/pride'
3
3
  require 'rep'
4
-
5
- class User < Struct.new(:name, :email, :location)
6
- end
7
-
8
- class Photo < Struct.new(:name, :location, :exif)
9
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rep
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-10 00:00:00.000000000 Z
12
+ date: 2012-11-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
@@ -27,6 +27,70 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rocco
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rdiscount
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
30
94
  description: A library for writing authoritative representations of objects for pages
31
95
  and apis.
32
96
  email:
@@ -36,10 +100,16 @@ extensions: []
36
100
  extra_rdoc_files: []
37
101
  files:
38
102
  - .gitignore
103
+ - .travis.yml
39
104
  - Gemfile
40
105
  - LICENSE.txt
41
106
  - README.md
42
107
  - Rakefile
108
+ - docs/index.html
109
+ - docs/lib/rep.html
110
+ - docs/lib/rep/version.html
111
+ - docs/rep.html
112
+ - docs/rep/version.html
43
113
  - lib/rep.rb
44
114
  - lib/rep/version.rb
45
115
  - rep.gemspec