HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyMinor

Ruby metaprogramming - declaring "properties" on a class

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
propertiesmetaprogrammingdeclaringrubyclass

Problem

I'm new to Ruby and trying to do a bit of metaprogramming. I want to declare some "properties" on a class, describing some field names and their access privileges:

class Resource
  # my_property(name, privilege => level)
  my_property :id, :read => :admin # :write => :none
  my_property :name, :read => :all, :write => :owner
  my_property :pin, :read => :owner, :write => :owner

  # ...
end


Where privileges are various symbols and levels are [:all, :owner, :admin], and higher level implies all lower levels.

Later, I want to retreive the information in this way:

res.readable_properties(:admin) # => [:id, :name, :pin]
res.writeable_properties(:admin) # => [:name, :pin]
res.writeable_properties(:all) # => []


Where method names are based on privilege names plus "able_properties" suffix.

Obviously, those declarations are on class level, so everything should be done only once when class is declared, and not on every instance creation.

I've tried to implement, but while it works fine, I suspect that my code is ugly and kludgy. Could someone please give me a hints on how to improve it? Here's what I came with:

require 'set'

class ResourceBase
  class  :admin #, :write => :none
  my_property :name, :read => :all, :write => :owner
  my_property :pin, :read => :owner, :write => :owner
end

resource = Resource.new
puts "Readable: " + resource.readable_properties(:admin).to_s
puts "Writeable: " + resource.writeable_properties(:admin).to_s


I seek for any suggestions which could be phrased as "Rather than [how OP did it] it's better to [how it should be done]."

Solution

Rather than access the possibly uninitialized @permissions instance variable inside my_property, use your wrapper method permissions and put the initialization logic in there. Also use Hash.new to save you the ||= statements. Also, the name list should be a Set, IMHO:

def permissions
  @permissions ||= Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = Set.new }}
end


That way you can simply call

permissions[permission][level].add


without bothering if the intermediate hashes and sets have been initialized

Some other thoughts:

  • The second half of your my_property method is only about updating the access methods. This should be a separate method.



  • You don't use @perm_methods at all, you can remove it.



  • Use map(&:to_sym) (or even better, don't create the temporary string array in the first place, it's not shorter!).



  • Don't use to_sym on the first argument to define_method, it shouldn't be necessary.



-
The min_level part can be improved in style and efficiency. You have at least two options here to get all the affected levels:

  • Use array slicing: access_levels[access_levels.index(level)..-1]



  • Use drop_while: access_levels.drop_while { |level| level != min_level }



After cleaning this up a bit, I came around with the following (untested) code:

require 'set'

class ResourceBase
  class  :admin #, :write => :none
  my_property :name, :read => :all, :write => :owner
  my_property :pin, :read => :owner, :write => :owner
end

resource = Resource.new
puts "Readable: " + resource.readable_properties(:admin).to_s
puts "Writeable: " + resource.writeable_properties(:admin).to_s

Code Snippets

def permissions
  @permissions ||= Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = Set.new }}
end
permissions[permission][level].add
require 'set'

class ResourceBase
  class << self
    def my_property(name, access)
      access_levels = [ :all, :owner, :admin ]
      puts "#{name}: #{access}"

      access.each do |perm, min_level|
        access_levels.drop_while { |level| level != min_level }.each do |level|
          permissions[perm][level].add name
        end
      end
      update_permission_methods
    end

    def update_permission_methods
      permissions.each do |name, perm|
        define_method("#{name}able_properties") { |level| perm[level] }
      end
    end

    def permissions
      @permissions ||= Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = Set.new }}
    end
  end
end

class Resource < ResourceBase
  # Levels range: all < owner < admin
  # Higher levels imply lower ones (i.e. admin always have owner access)
  my_property :id, :read => :admin #, :write => :none
  my_property :name, :read => :all, :write => :owner
  my_property :pin, :read => :owner, :write => :owner
end

resource = Resource.new
puts "Readable: " + resource.readable_properties(:admin).to_s
puts "Writeable: " + resource.writeable_properties(:admin).to_s

Context

StackExchange Code Review Q#8765, answer score: 3

Revisions (0)

No revisions yet.