5
\$\begingroup\$

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 << self
    def my_property(name, access)
      access_levels = %w[all owner admin].map {|s| s.to_sym}
      puts "#{name}: #{access}"

      @permissions ||= {}
      permit = {}
      access_levels.each do |level|
        access.each do |permission, min_level|
          @permissions[permission] ||= {}
          @permissions[permission][level] ||= []
          permit[permission] = true if level == min_level
          @permissions[permission][level].push(name) if permit[permission]
        end
      end

      @perm_methods ||= Set.new()
      @permissions.keys.each do |permission|
        next if @perm_methods.include? permission
        @perm_methods.add permission
        define_method "#{permission}able_properties".to_sym, do |level|
          self.class.permissions[permission][level]
        end
      end
    end

    def permissions
      @permissions
    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

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]."

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

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:

    1. Use array slicing: access_levels[access_levels.index(level)..-1]
    2. 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 << 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
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for a great explaination. I wonder, if re-defining methods multiple times is considered okay? In your code, readable_properties is redefined each time a "readable" property is declared (puts "defining #{name}able_properties" in update_permission_methods shows it). This is why I've used @perm_methods (although I forgot to add elements to it, shame on me). \$\endgroup\$
    – drdaeman
    Feb 7, 2012 at 22:02
  • 1
    \$\begingroup\$ @drdaeman: Hm, it doesn't give me a warning, so it's at least possible. It'd probably be better to check whether the method is already present first. \$\endgroup\$
    – Niklas B.
    Feb 7, 2012 at 22:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.