Dynamic class resolution?

The following is going to start off sounding Rails-specific, but if you
read on, it turns into a Ruby question.

I’m in an interesting situation recently where it was suggested
internally that we introduce a concept in our Rails application somewhat
similar to Java’s entity reference:
http://docs.oracle.com/javase/1.5.0/docs/api/org/w3c/dom/EntityReference.html

But, really, the intent was the following:

  1. The normal version of a model had all its fields and was writable.
  2. The “ref” version of a model exposed a subset of its fields for
    deserialization into JSON and would be read-only.

Beyond that, the desire was to have a concept similar to a model that
has some attributes and associations that can be both
updated/mass-assigned and deserialized into JSON, but also have some
attributes write only or de-serialize only (read-only). I posed that to
the Rails core list and it looks like Roar’s representers might work for
that. The other thing they want is to allow patch updates easily via
JSON service in a way that easily works with AngularJS, Ember, etc. to
allow certain columns of certain rows to be updated in the DB together
as a server-side DB transaction in various tables at once without
overwriting other columns in other columns of those same records. (In
addition, internally we wanted the JSON API in Rails to not require
_attributes at the end of JSON key names for attributes and to allow
passing in child JSON for association names and only have it look at the
id attribute in child JSON from an update to change associations vs.
using accepts_nested_attributes_for, but I implemented that already in a
project called restful_json in GitHub.)

To try to handle #1 and #2 above, at first I tried to implement by just
subclassing model and then having the model class have an association to
another model and that other model have an association to the “ref”
model instead of back to the first model, but I ran into a strange bug
when using subclass there (not a Ruby bug, but possibly an A.R. bug):

I ended up with something similar to the attached example.rb (split into
multiple files).

This may go away if we switch to Roar or similar, but for now, having a
separate class that differs only slightly from the original, it seems
like it would be helpful to be able to define something in Ruby that
would act like a class name pattern matcher so that when I referred to a
“FooRef” class, it would take the “Foo” class and add to it. This would
substantially reduce the number of files we would have to have to order
to have the concept of “*Ref” model classes.

An alternate way that would use existing Ruby and just extend
ActiveRecord to redefine associations such that you could pass options
into them that would try to override Rails class_accessors like we are
doing currently, e.g.:
self._accessible_attributes[:default] = [:name]

But, I was just curious- is there an existing way to hook into Ruby such
that you can say something like “if anyone asks for a classname that
ends in ‘Ref’ then take the class matching the first part of the name
and include or extend this and that”? Then you wouldn’t even have to
define the *Ref classes- they would be created and instantiated
dynamically.

I know that in this case, that probably wouldn’t work, since there would
be fields that would be specific to each Ref that at the very least
would have to be defined as part of the classname like:
“SomeRef{options…}”
where options would not be for initialize, but something available in
the scope of “Some” class’s block that it could use, perhaps.

I guess the subject of the post was poorly titled. It probably should
have been “dynamic class definition by classname?”

You probably want to look at const_missing. Watch out and make sure you
always call super because rails uses it to be lazy about requires.

Ok, I figured it out and put it up on GitHub and Rubygems if anyone is
interested. It doesn’t look exactly like what I was saying, but does
basically the same thing:
https://github.com/garysweaver/classmeta

Thanks again!

Ryan D. wrote in post #1073629:

You probably want to look at const_missing. Watch out and make sure you
always call super because rails uses it to be lazy about requires.

Thanks, Ryan! Looked into that more yesterday and learned about
ActiveSupport::Dependencies:
https://github.com/rails/rails/blob/3-2-stable/activesupport/lib/active_support/dependencies.rb

It’s funny after using Rails so much that I didn’t give a lot of thought
how class and module resolution work.

So, I thought I’d try to be crafty and throw together a gem to load in
Rails 3.2 that would do this (among other things):

class Object
class << self
class << self
@ref_classes={}

  puts "renaming const_missing to object_const_missing in #{self}"
  raise "Not on the right class. this is #{self.class.name}" unless 

instance_methods.include?(:const_missing)
alias_method(:object_const_missing, :const_missing)
undef_method(:const_missing)
raise “didn’t undefine const_missing on #{self.class.name}” if
instance_methods.include?(:const_missing)
raise “didn’t change name of const_missing to object_const_missing
on #{self.class.name}” unless
instance_methods.include?(:object_const_missing)

  def const_missing(name)
    puts "in my const_missing in #{self}"
    return @ref_classes[name] if @ref_classes[name]
    if name.to_s.end_with?('Ref')
      orig = const_get(name.to_s.chomp('Ref'))
      if orig && orig.ancestors.include?(ActiveRecord::Base)
        ref = orig.dup
        ref.class_eval('include ActiveRecordRefs::Model')
        return ref
      else
        object_const_missing(name)
      end
    end
  end

  raise "should have been able to redefine const_missing on 

#{self.class.name}" unless instance_methods.include?(:const_missing)
end
end
end

So, that loads but it is completely ignored, and this morning I read
this:
http://wondible.com/2011/12/23/give-rails-autoloading-a-boot-to-the-head/

Getting closer. So my new goal would be to have something that allows
new class “creation” via const_missing that strips off the end and does
something with it, e.g. if an association has class_name:
‘Person[RO,attr[first_name,last_name]]’ then const_missing would take
that and do a Person.dup and then pass the Person.dup class (named
‘Person[RO,attr[:first_name,:last_name]]’) off to a user-configurable
class’s class method that could do something with it like:

if the RO token is found would use the be_readonly gem’s functionality

new_class.class_eval(‘include BeReadonly::Now’)

parse out attrs and do

new_class.class_eval(“self._accessible_attributes[:default] =
#{attrs.collect{|a|a.to_sym}.inspect}”)

It may be too magic, but it would be a way to use associations to have
each model easily define new classes and functionality to tack on to
existing models, and would be a lot more DRY. The reason is because
we’re developing another gem that reuses mass assignment security to
define attributes that are serializable to JSON, along with another
custom class_attribute that specifies attributes and associations that
are only serialized.

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs