Module: Rep::ClassMethods

Defined in:
lib/rep.rb

Instance Method Summary collapse

Instance Method Details

#all_json_fieldsObject

We need a way to get a flat, uniq’ed list of all the fields accross all field sets. This is that.



218
219
220
# File 'lib/rep.rb', line 218

def all_json_fields
  flat_json_fields(:left)
end

#all_json_methodsObject

We need a wya to get a flat, uniq’ed list of all the method names accross all field sets. This is that.



224
225
226
# File 'lib/rep.rb', line 224

def all_json_methods
  flat_json_fields(:right)
end

#flat_json_fields(side = :right) ⇒ Object

‘#flat_json_fields` is just a utility method to DRY up the next two methods, because their code is almost exactly the same, it is not intended for use directly and might be confusing.



202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/rep.rb', line 202

def flat_json_fields(side = :right)
  side_number = side == :right ? 1 : 0

  json_fields.reduce([]) do |memo, (name, fields)|
    memo + fields.map do |field|
      if field.is_a?(Hash)
        field.to_a.first[side_number] # [name, method_name]
      else
        field
      end
    end
  end.uniq
end

#initialize_with(*args, &blk) ⇒ Object

Defines an ‘#initialize` method that accepts a Hash argument and copies some keys out into `attr_accessors`. If your class already has an `#iniatialize` method then this will overwrite it (so don’t use it). ‘#initialize_with` does not have to be used to use any other parts of Rep.



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/rep.rb', line 122

def initialize_with(*args, &blk)
  @initializiation_args = args

  # Remember what args we normally initialize with so we can refer to them when building shared instances.

  if defined?(define_singleton_method)
    define_singleton_method :initializiation_args do
      @initializiation_args
    end
  else
    singleton = class << self; self end
    singleton.send :define_method, :initializiation_args, lambda { @initializiation_args }
  end

  # Create an `attr_accessor` for each one. Defaults can be provided using the Hash version { :arg => :default_value }

  args.each { |a| register_accessor(a) }

  define_method(:initialize) { |opts={}|
    parse_opts(opts)
  }

  # `#parse_opts` is responsable for getting the `attr_accessor` values prefilled. Since defaults can be specified, it
  # must negotiate Hashes and use the first key of the hash for the `attr_accessor`'s name.

  define_method :parse_opts do |opts|
    @rep_options = opts
    blk.call(opts) unless blk.nil?
    self.class.initializiation_args.each do |field|
      name = field.is_a?(Hash) ? field.to_a.first.first : field
      instance_variable_set(:"@#{name}", opts[name])
    end
  end
end

#json_fields(arg = nil) ⇒ Object

‘#json_fields` setups up some class instance variables to remember sets of top level keys for json structures. Example:

class A
  json_fields [:one, :two, :three] => :default
end

A.json_fields(:default) # => [:one, :two, :three]

There is a general assumption that each top level key’s value is provided by a method of the same name on an instance of the class. If this is not true, a Hash syntax can be used to alias to a different method name. Example:

class A
  json_fields [{ :one => :the_real_one_method }, :two, { :three => :some_other_three }] => :default
end

Once can also set multiple sets of fields. Example:

class A
  json_fields [:one, :two, :three] => :default
  json_fields [:five, :two, :six] => :other
end

And all fields are returned by calling ‘#json_fields` with no args. Example:

A.json_fields # => { :default => [:one, :two, :three], :other => [:five, :two, :six] }


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/rep.rb', line 183

def json_fields(arg = nil)
  if arg.is_a?(Hash)
    fields, name = arg.to_a.first
    @json_fields ||= {}
    @json_fields[name] = [fields].flatten
  elsif arg.is_a?(Symbol)
    @json_fields ||= {}
    @json_fields[arg]
  elsif arg === nil
    @json_fields || {}
  else
    # TODO: make an exception class
    raise "You can only use a Hash to set fields, a Symbol to retrieve them, or no argument to retrieve all fields for all names"
  end
end

#register_accessor(acc) ⇒ Object

Defines an attr_accessor with a default value. The default for default is nil. Example:

class A
  register_accessor :name => "No Name"
end

A.new.name # => "No Name"


107
108
109
110
111
112
113
114
115
116
# File 'lib/rep.rb', line 107

def register_accessor(acc)
  name, default = acc.is_a?(Hash) ? acc.to_a.first : [acc, nil]
  attr_accessor name
  if default
    define_method name do
      var_name = :"@#{name}"
      instance_variable_get(var_name) || instance_variable_set(var_name, default)
    end
  end
end

#shared(opts = {}) ⇒ Object

An easy way to save on GC is to use the same instance to turn an array of objects into hashes instead of instantiating a new object for every object in the array. Here is an example of it’s usage:

class BookRep
  initialize_with :book_model
  fields :title => :default
  forward :title => :book_model
end

BookRep.shared(:book_model => Book.first).to_hash # => { :title => "Moby Dick" }
BookRep.shared(:book_model => Book.last).to_hash  # => { :title => "Lost Horizon" }

This should terrify you. If it doesn’t, then this example will:

book1 = BookRep.shared(:book_model => Book.first)
book2 = BookRep.shared(:book_model => Book.last)

boo1.object_id === book2.object_id # => true

**It really is a shared object.**

You really shouldn’t use this method directly for anything.



251
252
253
254
255
256
257
# File 'lib/rep.rb', line 251

def shared(opts = {})
  @pointer = (Thread.current[:rep_shared_instances] ||= {})
  @pointer[object_id] ||= new
  @pointer[object_id].reset_for_json!
  @pointer[object_id].parse_opts(opts)
  @pointer[object_id]
end

#to_procObject

The fanciest thing in this entire library is this ‘#to_proc` method. Here is an example of it’s usage:

class BookRep
  initialize_with :book_model
  fields :title => :default
  forward :title => :book_model
end

Book.all.map(&BookRep) # => [{ :title => "Moby Dick" }, { :title => "Lost Horizon " }]

And now I will explain how it works. Any object can have a to_proc method and when you call ‘#map` on an array and hand it a proc it will in turn hand each object as an argument to that proc. What I’ve decided to do with this object is use it the options for a shared instance to make a hash.

Since I know the different initialization argumants from a call to ‘initialize_with`, I can infer by order which object is which option. Then I can create a Hash to give to `parse_opts` through the `shared` method. I hope that makes sense.

It allows for extremely clean Rails controllers like this:

class PhotosController < ApplicationController
  respond_to :json, :html

  def index
    @photos = Photo.paginate(page: params[:page], per_page: 20)
    respond_with @photos.map(&PhotoRep)
  end

  def show
    @photo = Photo.find(params[:id])
    respond_with PhotoRep.new(photo: @photo)
  end
end


293
294
295
296
297
298
299
300
# File 'lib/rep.rb', line 293

def to_proc
  proc { |obj|
    arr = [obj].flatten
    init_args = @initializiation_args[0..(arr.length-1)]
    opts = Hash[init_args.zip(arr)]
    shared(opts).to_hash
  }
end