In a custom DataMapper setter, I'd like to check whether the value I'm setting is valid or not.
For instance:
class ToastMitten
include DataMapper::Resource
property :id, Serial
property :wearer, Enum['Chuck Norris', 'Jon Skeet']
property :first_worn_at, DateTime
def wearer=(name)
super
if wearer.valid? # How can I do this?
first_worn_at = Time.now
end
end
end
t = ToastMitten.new
t.wearer = 'Nathan Long' # invalid value; do NOT set first_worn_at
t.wearer = 'Jon Skeet' # valid value; set first_worn_at
Can I check the validity of a single property like this without calling valid? on the object itself and looking through all the errors?
I'm trying to figure this out myself, here is the best solution I've found so far:
While I haven't found a method to check the validity of a single property, as in:
t.wearer.valid?
I have found that you can check the validity of the entire object prior to saving, and then check if there are errors on the property you are interested in like so:
if t.valid?
# Everything is valid.
else
# There were errors, let's see if there were any on the 'wearer' property...
puts t.errors.on(:wearer)
end
I know that that isn't necessarily the answer you seek, but it's the best I've come up with so far. I'll post back if I find something better.
Related
I have the following Ruby code:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = ##SiteName_MONEY_PIT
def other_unimportant_stuff
whatever
end
end
end
So, what I'm trying to do here is set the SiteName and then use SiteName and combine it with the string _MONEY_PIT so I can access FOO1_MONEY_PIT and store its contents (500 in this case) in #site_num. Of course, the above code doesn't work, but there must be a way I can do this?
Thanks!!
If you want to dynamically get the value of a constant, you can use Module#const_get:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = BigTime.const_get(:"#{##SiteName}_MONEY_PIT")
end
end
Do not, under any circumstance, use Kernel#eval for this. Kernel#eval is extremely dangerous in any context where there is even the slightest possibility that an attacker may be able to control parts of the argument.
For example, if a user can choose the name of the site, and they name their site require 'fileutils'; FileUtils.rm_rf('/'), then Ruby will happily evaluate that code, just like you told it to!
Kernel#eval is very dangerous and you should not get into the habit of just throwing an eval at a problem. It is a very specialized tool that should only be employed when there is no other option (spoiler alert: there almost always is another option), and only after a thorough security review.
Please note that dynamically constructing variable names is already a code smell by itself, regardless of whether you use eval or not. It pretty much always points to a design flaw somewhere. In general, you can almost guaranteed replace the multiple variables with a data structure. E.g. in this case something like this:
module BigTime
MONEY_PITS = {
'FOO1' => 500,
'FOO2' => 501,
}.freeze
class LoseMoney
##SiteName = 'FOO1'
#site_num = MONEY_PITS[##SiteName]
end
end
You can refactor this as to use a Hash for your name lookups, and a getter method to retrieve it for easy testing/validation. For example:
module BigTime
MONEY_PITS = { FOO1: 500, FOO2: 501 }
MONEY_PIT_SUFFIX = '_MONEY_PIT'
class LoseMoney
##site = :FOO1
def initialize
site_name
end
def site_name
#site_name ||= '%d%s' % [MONEY_PITS[##site], MONEY_PIT_SUFFIX]
end
end
end
BigTime::LoseMoney.new.site_name
#=> "500_MONEY_PIT"
I currently generate a user's profile page using their serial ID, like so:
get '/users/:id' do
#user = User.get(params[:id])
end
This works great, until a number is entered that doesn't exist in the database.
I'm aware I can change User.get to User.get! to return an ObjectNotFoundError error if the record isn't found, but I'm not sure how I can use this to my aid.
I used to use .exists? when using RoR.
Any suggestions?
Thanks in advance!
Edit: I'm going to leave the question unanswered, as I haven't actually found a solution to what I asked in the title; however, I did manage to solve my own problem by checking to see if the :id entered is higher than the amount of users that exist in the database, like so:
if params[:id].to_i > User.count
"This user does not exist."
else
#users_id = User.get(params[:id])
erb(:'users/id')
end
You already have the correct code:
#user = User.get(params[:id])
If no user exists with the given id, then #get will return nil. Then you can just do a conditional:
#user = User.get params[:id]
if #user
# user exists
else
# no user exists
end
This is a very common pattern in Ruby which takes advantage of the "truthiness" of anything other than false or nil. i.e. you can say if 0 or if [] and the condition will evaluate to true
You can recreate a .exists? method:
class User
def self.exists?(id_or_conditions)
if id_or_conditions.is_a? Integer
!! User.get id_or_conditions
else
!! User.first id_or_conditions
end
end
end
#get is similar to #find in rails, except it doesn't raise an error if the record is not found. #first is similar to #find_by in rails.
Is it possible to update a property in DataMapper if :key is set to true?
Say, for example, I have a model set up like this:
class Post
include DataMapper::Resource
property :slug, Text, :unique => true, :key => true
# ...
end
and I made a new instance of this with :slug => "example-post-title".
I tried to update it by accessing the stored
#post = Post.get("example-post-title")
#=> #<Post #slug="example-post-title" ...>
#post.slug = "example-post-title-2"
#=> "example-post-title-2"
#post.save
#=> true
#post = Post.get("example-post-title-2")
#=> nil
but as you can see the slug was never updated. I also tried using the Post#update method:
#post = Post.get("example-post-title")
#=> #<Post #slug="example-post-title" ...>
#post.update(:slug => "example-post-title-2")
#=> true
#post = Post.get("example-post-title-2")
#=> nil
Looking in the database, the index column is not changed by either of these examples. It remains as example-post-title rather than example-post-title-2.
According to the docs, the Post#update method, similar to the Post#save method, should return true if the operation was successful, and false if it was not. It is returning true here, but it's not actually updating the record.
I've searched and searched and can't find anything about it on the Internet. Neither StackOverflow nor the DataMapper rdoc had anything about updating the key.
I know that I can have a unique Serial property and just get instances of Post using the slug (as in, make the Serial property the key instead of the slug), but I'm looking for a way to do it without that, if at all possible.
My hunch is that you can't update a key. According to the doc, they are protected against mass assignment:
Natural Keys are protected against mass-assignment, so their setter= will need to be called individually if you're looking to set them.
They don't talk about updating them but usually in "key => value" stores it is impossible or deprecated to update the key. I'd assume that's the case here as well, even though I can't find any hard evidence to give to you : /
I recently replaced a home-grown configuration module with Configatron, but I'm unable to get one use case working.
When I attempt to use a configatron value as an argument to Object.const_get like this:
def formatter_class
Object.const_get(configatron.formatter)
end
I get the following error:
file.rb:10:in `const_get': can't convert Configatron::Store to String
(Configatron::Store#to_str gives Configatron::Store) (TypeError)
The configatron assignment looks like this (simplified):
configatron.formatter = case
when condition?
'ExportFormat'
else
'ScreenFormat'
end
Even if I do configatron.formatter = 'ScreenFormat', I get the same error.
I've tried variations on the formatter_class method too. This fails:
def formatter_class
Object.const_get(configatron['formatter'])
end
Of course, this succeeds, but won't fulfill my use case:
def formatter_class
Object.const_get('ScreenFormat')
end
What am I doing wrong?
I solved my issue. Turns out you can call configatron.whatever and it will return a Configatron::Store if it's not initialized.
I inserted a call to configatron.has_key? 'formatter' before accessing the value. When it returned false, I figured out that the error was occurring in a code path where the value hadn't been initialized yet. Once I initialized the value, the error no longer occurs.
Happens when .yml config file is missing. Or the key that you are looking for is not there.
Location:
/config/NAME.yml
I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.
Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end
this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!
try .methods or .instance_variables
Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};
I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)