rep 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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