rep.rb |
|
---|---|
Rep is a small module to endow any class to make json quickly. It solves four problems:
The code is available on github. |
|
|
require 'forwardable' |
|
require 'json'
require 'rep/version'
module Rep |
All classes that |
def self.included(klass)
klass.extend Forwardable
klass.extend ClassMethods
klass.instance_eval {
class << self
unless defined?(forward)
alias forward delegate
end
unless defined?(fields)
alias fields json_fields
end
end
if defined?(Mashed)
include MashedSupport
end
}
end |
Since a goal is to be able to share instances, we need an easy way to reset a shared instance back to factory defaults. If you memoize any methods that are not declared as json fields, then overried this method and set any memoized variables to nil, then super. |
def reset_for_json!
self.class.all_json_methods.each do |method_name|
instance_variable_set(:"@#{method_name}", nil)
end
end |
All the work of generating a hash from an instance is packaged up in one method. Since
fields can be aliases in the format
Right now it will raise if either a field doesn’t have a method to provide it’s value or
if there are no json fields setup for the particular set (which defaults to |
def to_hash(name = :default)
if fields = self.class.json_fields(name)
fields.reduce({}) do |memo, field|
field_name, method_name = field.is_a?(Hash) ? field.to_a.first : [field, field]
begin
memo[field_name] = send(method_name)
rescue NoMethodError => e
message = "There is no method named '#{method_name}' for the class '#{self.class}' for the '#{name}' list of fields : #{e.message}"
raise NoMethodError.new(message, method_name, e.args)
end
memo
end
else
raise "There are no json fields under the name: #{name}"
end
end
def to_json
JSON.generate(to_hash)
end
module ClassMethods |
Defines an attr_accessor with a default value. The default for default is nil. Example:
|
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 |
Defines an |
def initialize_with(*args)
@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 |
args.each { |a| register_accessor(a) }
define_method(:initialize) { |*args|
opts = args.first || {}
parse_opts(opts)
} |
|
define_method :parse_opts do |opts|
@rep_options = opts
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 |
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:
Once can also set multiple sets of fields. Example:
And all fields are returned by calling
|
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 |
|
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 |
We need a way to get a flat, uniq'ed list of all the fields accross all field sets. This is that. |
def all_json_fields
flat_json_fields(:left)
end |
We need a wya to get a flat, uniq'ed list of all the method names accross all field sets. This is that. |
def all_json_methods
flat_json_fields(:right)
end |
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:
This should terrify you. If it doesn’t, then this example will:
It really is a shared object. You really shouldn’t use this method directly for anything. |
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 |
The fanciest thing in this entire library is this
And now I will explain how it works. Any object can have a to_proc method and when you call Since I know the different initialization argumants from a call to It allows for extremely clean Rails controllers like this:
|
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
end
module MashedSupport
def to_hash(name = :default)
Mashed::Mash.new(super)
end
end
end |