How to implement an Mongoid Document like class? - ruby

I'm building a small script where I'd need to implement a Mongoid Document like class, where I include my base module and then I can build a class which looks like:
class MyClass
include MyBaseModule
field :some_field, :attr => 'attributes'
end
This is my last try:
module Model
def initialize(keys = {})
puts ##keys
end
def method_missing sym, *args
if sym =~ /^(\w+)=$/
if ##keys.has_key?($1)
##keys[$1.to_sym] = args[0]
else
nil
end
else
if ##keys.has_key?($1)
##keys[sym.to_sym]
else
nil
end
end
end
def inspect
puts "#<#{self.class} #keys=#{##keys.each {|k,v| "#{k} => #{v}"}}>"
end
def self.included(base)
base.extend(ClassMethods)
end
def save
##keys.each do |k, v|
SimpleMongo::connection.collection.insert({k => v})
end
end
module ClassMethods
def field(name, args)
if ##keys.nil?
##keys = {}
end
##keys[name.to_sym] = default_value
end
end
end
Mongoid documents look like this:
class StoredFile
include Mongoid::Document
field :name, type: String
field :description, type: String
field :password, type: String
field :public, type: Boolean
field :shortlink, type: String
mount_uploader :stored_file, StoredFileUploader
before_save :gen_shortlink
before_save :hash_password
belongs_to :user
def gen_shortlink
self.shortlink = rand(36**10).to_s(36)
end
def public?
self.public
end
def hash_password
require 'bcrypt'
self.password = BCrypt::Password.create(self.password).to_s
end
def check_pass(password)
BCrypt::Password.new(self.password) == password
end
end
It doesn't works because the ##keys variable inside ClassMethods isn't available anywhere outside that module. What would be the easiest way to implement this? Thanks!

The easiest way to implement it would be to have a class variable getter.
module Model
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def keys
#keys ||= {}
end
def field(name, opts)
#keys ||= {}
#keys[name] = opts
end
end
def initialize(attributes)
# stuff
puts self.class.keys
end
end

Related

Creating Ruby builder object with re-usable code

I'm working to create a few Ruby builder objects, and thinking on how I could reuse some of Ruby's magic to reduce the logic of the builder to a single class/module. It's been ~10 years since my last dance with the language, so a bit rusty.
For example, I have this builder:
class Person
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
def build
output = {}
PROPERTIES.each do |prop|
if self.respond_to?(prop) and !self.send(prop).nil?
value = self.send(prop)
# if value itself is a builder, evalute it
output[prop] = value.respond_to?(:build) ? value.build : value
end
end
output
end
def method_missing(m, *args, &block)
if m.to_s.start_with?("set_")
mm = m.to_s.gsub("set_", "")
if PROPERTIES.include?(mm.to_sym)
self.send("#{mm}=", *args)
return self
end
end
end
end
Which can be used like so:
Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}
I would like to be able to refactor everything to a class and/or module so that I could create multiple such builders that'll only need to define attributes and inherit or include the rest (and possibly extend each other).
class BuilderBase
# define all/most relevant methods here for initialization,
# builder attributes and object construction
end
module BuilderHelper
# possibly throw some of the methods here for better scope access
end
class Person < BuilderBase
include BuilderHelper
PROPERTIES = [:name, :age, :email, :address]
attr_accessor(*PROPERTIES)
end
# Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").build
class Server < BuilderBase
include BuilderHelper
PROPERTIES = [:cpu, :memory, :disk_space]
attr_accessor(*PROPERTIES)
end
# Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").build
I've been able to get this far:
class BuilderBase
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
end
class Person < BuilderBase
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def build
...
end
def method_missing(m, *args, &block)
...
end
end
Trying to extract method_missing and build into the base class or a module keeps throwing an error at me saying something like:
NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES
Essentially the neither the parent class nor the mixin are able to access the child class' attributes. For the parent this makes sense, but not sure why the mixin can't read the values inside the class it was included into. This being Ruby I'm sure there's some magical way to do this that I have missed.
Help appreciated - thanks!
I reduced your sample to the required parts and came up with:
module Mixin
def say_mixin
puts "Mixin: Value defined in #{self.class::VALUE}"
end
end
class Parent
def say_parent
puts "Parent: Value defined in #{self.class::VALUE}"
end
end
class Child < Parent
include Mixin
VALUE = "CHILD"
end
child = Child.new
child.say_mixin
child.say_parent
This is how you could access a CONSTANT that lives in the child/including class from the parent/included class.
But I don't see why you want to have this whole Builder thing in the first place. Would an OpenStruct not work for your case?
Interesting question. As mentioned by #Pascal, an OpenStruct might already do what you're looking for.
Still, it might be more concise to explicitly define the setter methods. It might also be clearer to replace the PROPERTIES constants by methods calls. And since I'd expect a build method to return a complete object and not just a Hash, I renamed it to to_h:
class BuilderBase
def self.properties(*ps)
ps.each do |property|
attr_reader property
define_method :"set_#{property}" do |value|
instance_variable_set(:"##{property}", value)
#hash[property] = value
self
end
end
end
def initialize(**kwargs)
#hash = {}
kwargs.each do |k, v|
self.send("set_#{k}", v) if self.respond_to?(k)
end
end
def to_h
#hash
end
end
class Person < BuilderBase
properties :name, :age, :email, :address
end
p Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").to_h
# {:name=>"Joe", :age=>30, :email=>"joe#mail.com", :address=>"NYC"}
class Server < BuilderBase
properties :cpu, :memory, :disk_space
end
p Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").to_h
# {:cpu=>"i9", :memory=>"32GB", :disk_space=>"1TB"}
I think no need to declare PROPERTIES, we can create a general builder like this:
class Builder
attr_reader :build
def initialize(clazz)
#build = clazz.new
end
def self.build(clazz, &block)
builder = Builder.new(clazz)
builder.instance_eval(&block)
builder.build
end
def set(attr, val)
#build.send("#{attr}=", val)
self
end
def method_missing(m, *args, &block)
if #build.respond_to?("#{m}=")
set(m, *args)
else
#build.send("#{m}", *args, &block)
end
self
end
def respond_to_missing?(method_name, include_private = false)
#build.respond_to?(method_name) || super
end
end
Using
class Test
attr_accessor :x, :y, :z
attr_reader :w, :u, :v
def set_w(val)
#w = val&.even? ? val : 0
end
def add_u(val)
#u = val if val&.odd?
end
end
test1 = Builder.build(Test) {
x 1
y 2
z 3
} # <Test:0x000055b6b0fb2888 #x=1, #y=2, #z=3>
test2 = Builder.new(Test).set(:x, 1988).set_w(6).add_u(2).build
# <Test:0x000055b6b0fb23b0 #x=1988, #w=6>

Always capitalize attribute?

I have the following class:
class Message
extend ActiveModel::Naming
include ActiveModel::Conversion
attr_accessor :name, :emails, :custom_content
def initialize(attrs = {})
attrs.each do |k, v|
self.send "#{k}=", v
end
end
def persisted?
false
end
def email_list
self.emails.split(",").collect { |email| {:email => email.delete(" ")} }
end
end
I always want to capitalize the name attribute instead of having to always do it when I call the attribute in code.
Figured it out:
class Message
extend ActiveModel::Naming
include ActiveModel::Conversion
attr_accessor :name, :emails, :custom_content
def initialize(attrs = {})
attrs.each do |k, v|
self.send "#{k}=", v
end
end
def persisted?
false
end
#Fix is here
def name=(s)
#name = s.titleize
end
def email_list
self.emails.split(",").collect { |email| {:email => email.delete(" ")} }
end
end

Define a class method in a module with a variable name

I created a module which is included in a class. In the module, I am trying to define a method that is the downcased version of a class name without Filter. So ShowFilter would have a method called show that returns the class Show. I get "NoMethodError:
undefined method `show' for ShowFilter:Class"
module Filters
module Base
module ClassMethods
##filters = {}
def filter name, &block
##filters[name] = block
end
def run query = {}
query.each do |name, value|
##filters[name.to_sym].call(value) unless ##filters[name.to_sym].nil?
end
self
end
def self.extended(base)
name = base.class.name.gsub(/filter/i, '')
define_method(name.downcase.to_sym) { Kernel.const_get name }
end
end
def self.included base
base.extend ClassMethods
end
end
end
class ShowFilter
include Filters::Base
filter :name do |name|
self.show.where(:name => name)
end
end
EDIT: Example of use
class ShowController < ApplicationController
def index
ShowFilter.run params[:query]
end
end
When you define Filters::Base::ClassMethods, it evaluates self in that context so the method you'll end up defining is ClassMethods.classmethods (since the gsub won't do anything).
Like the included hook you tapped into in Base, you want to use extended in ClassMethods:
module Filters
module Base
module ClassMethods
##filters = {}
def filter name, &block
##filters[name] = block
end
def run query = {}
query.each do |name, value|
##filters[name.to_sym].call(value) unless ##filters[name.to_sym].nil?
end
Object.const_get(self.to_s.gsub('Filter', ''))
end
def self.extended(base)
define_method(base.to_s.downcase.gsub('filter', '').to_sym) do
Object.const_get(self.to_s.gsub('Filter', ''))
end
end
end
def self.included base
base.extend ClassMethods
end
end
end
class ShowFilter
include Filters::Base
filter :title do |title|
self.show.where(:title => title)
end
end

Customising attr_reader to do lazy instantiation of attributes

(Big edit, I got part of the way there…)
I've been hacking away and I've come up with this as a way to specify things that need to be done before attributes are read:
class Class
def attr_reader(*params)
if block_given?
params.each do |sym|
define_method(sym) do
yield
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
class Test
attr_reader :normal
attr_reader(:jp,:nope) { changethings if #nope.nil? }
def initialize
#normal = "Normal"
#jp = "JP"
#done = false
end
def changethings
p "doing"
#jp = "Haha!"
#nope = "poop"
end
end
j = Test.new
p j.normal
p j.jp
But changethings isn't being recognised as a method — anyone got any ideas?
You need to evaluate the block in the context of the instance. yield by default will evaluate it in its native context.
class Class
def attr_reader(*params, &blk)
if block_given?
params.each do |sym|
define_method(sym) do
self.instance_eval(&blk)
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
Here's another alternative approach you can look at. It's not as elegant as what you're trying to do using define_method but it's maybe worth looking at.
Add a new method lazy_attr_reader to Class
class Class
def lazy_attr_reader(*vars)
options = vars.last.is_a?(::Hash) ? vars.pop : {}
# get the name of the method that will populate the attribute from options
# default to 'get_things'
init_method = options[:via] || 'get_things'
vars.each do |var|
class_eval("def #{var}; #{init_method} if !defined? ##{var}; ##{var}; end")
end
end
end
Then use it like this:
class Test
lazy_attr_reader :name, :via => "name_loader"
def name_loader
#name = "Bob"
end
end
In action:
irb(main):145:0> t = Test.new
=> #<Test:0x2d6291c>
irb(main):146:0> t.name
=> "Bob"
IMHO changing the context of the block is pretty counter-intuitive, from a perspective of someone who would use such attr_reader on steroids.
Perhaps you should consider plain ol' "specify method name using optional arguments" approach:
def lazy_attr_reader(*args, params)
args.each do |e|
define_method(e) do
send(params[:init]) if params[:init] && !instance_variable_get("##{e}")
instance_variable_get("##{e}")
end
end
end
class Foo
lazy_attr_reader :foo, :bar, :init => :load
def load
#foo = 'foo'
#bar = 'bar'
end
end
f = Foo.new
puts f.bar
#=> bar

Fastest/One-liner way to list attr_accessors in Ruby?

What's the shortest, one-liner way to list all methods defined with attr_accessor? I would like to make it so, if I have a class MyBaseClass, anything that extends that, I can get the attr_accessor's defined in the subclasses. Something like this:
class MyBaseClass < Hash
def attributes
# ??
end
end
class SubClass < MyBaseClass
attr_accessor :id, :title, :body
end
puts SubClass.new.attributes.inspect #=> [id, title, body]
What about to display just the attr_reader and attr_writer definitions?
Extract the attributes in to an array, assign them to a constant, then splat them in to attr_accessor.
class SubClass < MyBaseClass
ATTRS = [:id, :title, :body]
attr_accessor(*ATTRS)
end
Now you can access them via the constant:
puts SubClass.ATTRS #=> [:id, :title, :body]
There is no way (one-liner or otherwise) to list all methods defined by attr_accessor and only methods defined by attr_accessor without defining your own attr_accessor.
Here's a solution that overrides attr_accessor in MyBaseClass to remember which methods have been created using attr_accessor:
class MyBaseClass
def self.attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def self.attributes
#attributes
end
def attributes
self.class.attributes
end
end
class SubClass < MyBaseClass
attr_accessor :id, :title, :body
end
SubClass.new.attributes.inspect #=> [:id, :title, :body]
Heres an alternative using a mixin rather than inheritance:
module TrackAttributes
def attr_readers
self.class.instance_variable_get('#attr_readers')
end
def attr_writers
self.class.instance_variable_get('#attr_writers')
end
def attr_accessors
self.class.instance_variable_get('#attr_accessors')
end
def self.included(klass)
klass.send :define_singleton_method, :attr_reader, ->(*params) do
#attr_readers ||= []
#attr_readers.concat params
super(*params)
end
klass.send :define_singleton_method, :attr_writer, ->(*params) do
#attr_writers ||= []
#attr_writers.concat params
super(*params)
end
klass.send :define_singleton_method, :attr_accessor, ->(*params) do
#attr_accessors ||= []
#attr_accessors.concat params
super(*params)
end
end
end
class MyClass
include TrackAttributes
attr_accessor :id, :title, :body
end
MyClass.new.attr_accessors #=> [:id, :title, :body]
Following up on Christian's response, but modifying to use ActiveSupport::Concern...
module TrackAttributes
extend ActiveSupport::Concern
included do
define_singleton_method(:attr_reader) do |*params|
#attr_readers ||= []
#attr_readers.concat params
super(*params)
end
define_singleton_method(:attr_writer) do |*params|
#attr_writers ||= []
#attr_writers.concat params
super(*params)
end
define_singleton_method(:attr_accessor) do |*params|
#attr_accessors ||= []
#attr_accessors.concat params
super(*params)
end
end
def attr_readers
self.class.instance_variable_get('#attr_readers')
end
def attr_writers
self.class.instance_variable_get('#attr_writers')
end
def attr_accessors
self.class.instance_variable_get('#attr_accessors')
end
end
Class definitions
class MyBaseClass
attr_writer :an_attr_writer
attr_reader :an_attr_reader
def instance_m
end
def self.class_m
end
end
class SubClass < MyBaseClass
attr_accessor :id
def sub_instance_m
end
def self.class_sub_m
end
end
Call as class methods
p SubClass.instance_methods - Object.methods
p MyBaseClass.instance_methods - Object.methods
Call as instance methods
a = SubClass.new
b = MyBaseClass.new
p a.methods - Object.methods
p b.methods - Object.methods
Both will output the same
#=> [:id, :sub_instance_m, :id=, :an_attr_reader, :instance_m, :an_attr_writer=]
#=> [:an_attr_reader, :instance_m, :an_attr_writer=]
How to tell which are writer reader and accessor?
attr_accessor is both attr_writer and attr_reader
attr_reader outputs no = after the method name
attr_writer outputs an = after the method name
You can always use a regular expression to filter that output.
It could be filtered with the equal sign matching logic:
methods = (SubClass.instance_methods - SubClass.superclass.instance_methods).map(&:to_s)
methods.select { |method| !method.end_with?('=') && methods.include?(method + '=') }
#> ["id", "title", "body"]

Resources