In Ruby/Sinatra, Datamapper's .all works but .get doesn't? - ruby

I am trying to take data from a path in Sinatra, and use it to look up a particular record using Datamapper. The Datamapper docs seem to indicate that.
get "/test/:test_path" do
test_get = Intake.get( params[:test_path] )
# Do stuff
erb :blah_blah_blah
end
should find any records associated with the symbol :test_path
This does not work. test_get gets nil.
Meanwhile, what does work is
get "/test/:test_path" do
test_all = Intake.all(:test_path => params[:test_path] )
# Do stuff
erb :blah_blah
end
My two questions are:
What am I doing wrong with the .get() call in Datamapper?
Is the .all(:name => value) method slower than .get(), or does it not matter which I use?
Here's a Sinatra script pared down to demonstrate the behavior.
#!/usr/bin/env ruby
require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-timestamps'
DataMapper.setup(:default, {:adapter => 'yaml', :path => 'db'})
class Intake
include DataMapper::Resource
property :id, Serial
property :created_at, DateTime
property :test_path, String
end
get "/test/:test_path" do
test_all = Intake.all(:test_path => params[:test_path] )
puts 'test_all:' test_all.inspect
test_get = Intake.get( params[:test_path] )
puts 'test_get:' test_get.inspect
"Hello World!"
end

#get only does a lookup based on primary key, with is the id. So
Intake.get(params[:test_path])
looks for something with id params[:test_path], which will fail. Use
Intake.first(test_path: params[:test_path])

Related

Use Geocoder with Sinatra and DataMapper

I'm attempting to use the Geocoder gem with a DataMapper model in a Sinatra application.
environment.rb:
require 'rubygems'
require 'bundler/setup'
require 'dm-core'
require 'dm-timestamps'
require 'dm-validations'
require 'dm-aggregates'
require 'dm-migrations'
require 'dm-types'
require 'geocoder'
require 'sinatra' unless defined?(Sinatra)
# load models
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
Dir.glob("#{File.dirname(__FILE__)}/lib/*.rb") { |lib| require File.basename(lib, '.*') }
DataMapper.setup(:default, (ENV["DATABASE_URL"] || "sqlite3:///#{File.expand_path(File.dirname(__FILE__))}/#{Sinatra::Base.environment}.db"))
DataMapper.finalize
DataMapper.auto_upgrade!
lib/location.rb:
class Location
include DataMapper::Resource
include Geocoder::Model::Base
property :id, Serial
property :address, String, :required => true
# geocoder gem
geocoded_by :address, :latitude => :lat, :longitude => :lng
# geocoder
after_validation :geocode, :if => :address_changed?
end
When I attempt to start an IRB session, an exception is generated:
irb> require './environment'
NameError: uninitialized constant Geocoder::Model
...
What am I not understanding?
First up, it looks like the Geocode gem won't have direct support for Datamapper, as per this issue.
Second, when you include a module inside a class, the methods are available to the instance of the class, and not at the class level. For example:
module Name
def name
puts "Module"
end
end
class SomeClass
include Name
end
SomeClass.new.name # => "Module"
This works because when you include a module, that module gets added to the ancestor chain of that class. Any methods that get sent to the instance which are not available on the instance are forwarded to the ancestors. However, there's another method called extend which adds the methods at a class-level rather than at instance level:
# module definition same as before
class SomeClass
extend Name
name # works!
end
Inorder to get class-level inclusion, there is another way (which is what the Geocoder gem uses for supported models:
# module code same as before
module Name
def name
puts "Module"
end
def self.included(klass)
klass.extend(self)
end
end
The included hook is provided for models which can be overridden to do something when the include Name step executes. Since there's no Datamapper specific module that is not executing this step, you see that error.

'Error: Cannot open "/home/<...>/billy-bones/=" for reading' while using pry and DataMapper

So, I'm trying to build a quick console program for my development needs, akin to rails console (I'm using Sinatra + DataMapper + pry).
I run it and launch cat = Category.new(name: 'TestCat', type: :referential). It gives me the following error:
Error: Cannot open "/home/art-solopov/Projects/by-language/Ruby/billy-bones/=" for reading.
What could be the cause of the problem?
console:
#!/usr/bin/env ruby
$LOAD_PATH << 'lib'
require 'pry'
require 'config'
binding.pry
lib/config.rb:
# Configuration files and app-wide requires go here
require 'sinatra'
require 'data_mapper'
require 'model/bill'
require 'model/category'
configure :production do
DataMapper::Logger.new('db-log', :debug)
DataMapper.setup(:default,
'postgres://billy-bones:billy#localhost/billy-bones')
DataMapper.finalize
end
configure :development do
DataMapper::Logger.new($stderr, :debug)
DataMapper.setup(:default,
'postgres://billy-bones:billy#localhost/billy-bones-dev')
DataMapper.finalize
DataMapper.auto_upgrade!
end
configure :test do
require 'dm_migrations'
DataMapper::Logger.new($stderr, :debug)
DataMapper.setup(:default,
'postgres://billy-bones:billy#localhost/billy-bones-test')
DataMapper.finalize
DataMapper.auto_migrate!
end
lib/model/category.rb:
require 'data_mapper'
class Category
include DataMapper::Resource
property :id, Serial
property :name, String
property :type, Enum[:referential, :predefined, :computable]
has n, :bills
# has n, :tariffs TODO uncomment when tariff ready
def create_bill(params)
# A bill factory for current category type
case type
when :referential
ReferentialBill.new params
when :predefined
PredefinedBill.new params
when :computable
ComputableBill.new params
end
end
end
If I substitute pry with irb in the console script, it goes fine.
Thank you very much!
P. S.
Okay, yesterday I tried this script again, and it worked perfectly. I didn't change anything. I'm not sure whether I should remove the question now or not.
P. P. S.
Or actually not... Today I've encountered it again. Still completely oblivious to what could cause it.
** SOLVED **
DAMN YOU PRY!
Okay, so here's the difference.
When I tested it the second time, I actually entered a = Category.new(name: 'TestCat', type: :referential) and it worked. Looks like pry just thinks cat is a Unix command, not a valid variable name.
Not answer to the pry question I just generally hate case statements in ruby.
Why not change:
def create_bill(params)
# A bill factory for current category type
case type
when :referential
ReferentialBill.new params
when :predefined
PredefinedBill.new params
when :computable
ComputableBill.new params
end
end
to:
def create_bill(params)
# A bill factory for current category type
self.send("new_#{type}_bill",params)
end
def new_referential_bill(params)
ReferentialBill.new params
end
def new_predefined_bill(params)
PredefinedBill.new params
end
def new_computable_bill(params)
ComputableBill.new params
end
You could make this more dynamic but I think that would take away from readability in this case but if you'd like in rails this should do the trick
def create_bill(params)
if [:referential, :predefined, :computable].include?(type)
"#{type}_bill".classify.constantize.new(params)
else
#Some Kind of Handling for non Defined Bill Types
end
end
Or this will work inside or outside rails
def create_bill(params)
if [:referential, :predefined, :computable].include?(type)
Object.const_get("#{type.to_s.capitalize}Bill").new(params)
else
#Some Kind of Handling for non Defined Bill Types
end
end

How can I serialize DataMapper::Validations::ValidationErrors to_json in Sinatra?

I'm developing a RESTful API using Sinatra and DataMapper. When my models fail validation, I want to return JSON to indicate what fields were in error. DataMapper adds an 'errors' attribute to my model of type DataMapper::Validations::ValidationErrors. I want to return a JSON representation of this attribute.
Here's a single file example (gotta love Ruby/Sinatra/DataMapper!):
require 'sinatra'
require 'data_mapper'
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String, :required => true
property :middle_name, String
property :last_name, String, :required => true
end
DataMapper.setup :default, 'sqlite::memory:'
DataMapper.auto_migrate!
get '/person' do
person = Person.new :first_name => 'Dave'
if person.save
person.to_json
else
# person.errors - what to do with this?
{ :errors => [:last_name => ['Last name must not be blank']] }.to_json
end
end
Sinatra::Application.run!
In my actual app, I'm handling a POST or PUT, but to make the problem easy to reproduce, I'm using GET so you can use curl http://example.com:4567/person or your browser.
So, what I have is person.errors and the JSON output I'm looking for is like what's produced by the hash:
{"errors":{"last_name":["Last name must not be blank"]}}
What do I have to do to get the DataMapper::Validations::ValidationErrors into the JSON format I want?
So, as I was typing this up, the answer came to me (of course!). I've burned several hours trying to figure this out, and I hope this will save others the pain and frustration I've experienced.
To get the JSON I'm looking for, I just had to create a hash like this:
{ :errors => person.errors.to_h }.to_json
So, now my Sinatra route looks like this:
get '/person' do
person = Person.new :first_name => 'Dave'
if person.save
person.to_json
else
{ :errors => person.errors.to_h }.to_json
end
end
Hope this helps others looking to solve this problem.
I know, I am answering this late, but, in case you are just looking for just validation error messages, you can use object.errors.full_messages.to_json. For example
person.errors.full_messages.to_json
will result in something like
"[\"Name must not be blank\",\"Code must not be blank\",
\"Code must be a number\",\"Jobtype must not be blank\"]"
This will rescue on client side from iterating over key value pair.

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

Sinatra Variable Scope

Take the following code:
### Dependencies
require 'rubygems'
require 'sinatra'
require 'datamapper'
### Configuration
config = YAML::load(File.read('config.yml'))
name = config['config']['name']
description = config['config']['description']
username = config['config']['username']
password = config['config']['password']
theme = config['config']['theme']
set :public, 'views/themes/#{theme}/static'
### Models
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/marvin.db")
class Post
include DataMapper::Resource
property :id, Serial
property :name, String
property :body, Text
property :created_at, DateTime
property :slug, String
end
class Page
include DataMapper::Resource
property :id, Serial
property :name, String
property :body, Text
property :slug, String
end
DataMapper.auto_migrate!
### Controllers
get '/' do
#posts = Post.get(:order => [ :id_desc ])
haml :"themes/#{theme}/index"
end
get '/:year/:month/:day/:slug' do
year = params[:year]
month = params[:month]
day = params[:day]
slug = params[:slug]
haml :"themes/#{theme}/post.haml"
end
get '/:slug' do
haml :"themes/#{theme}/page.haml"
end
get '/admin' do
haml :"admin/index.haml"
end
I want to make name, and all those variables available to the entire script, as well as the views. I tried making them global variables, but no dice.
Might not be the "cleanest" way to do it, but setting them as options should work:
--> http://www.sinatrarb.com/configuration.html :)
setting:
set :foo, 'bar'
getting:
"foo is set to " + settings.foo
Make them constants. They should be anyway shouldn't they? They're not going to change.
Make a constant by writing it in all caps.
Read this article on Ruby Variable Scopes if you have any more issues.
http://www.techotopia.com/index.php/Ruby_Variable_Scope
Another clean option may be a config class, where the init method loads the YAML and then sets up the variables.
Have fun. #reply me when you've finished your new blog (I'm guessing this is what this is for).
From the Sinatra README:
Accessing Variables in Templates
Templates are evaluated within the same context as route handlers. Instance variables set in route handlers are direcly accessible by templates:
get '/:id' do
#foo = Foo.find(params[:id])
haml '%h1= #foo.name'
end
Or, specify an explicit Hash of local variables:
get '/:id' do
foo = Foo.find(params[:id])
haml '%h1= foo.name', :locals => { :foo => foo }
end
This is typically used when rendering templates as partials from within other templates.
A third option would be to set up accessors for them as helper methods. (Which are also available throughout the application and views.)
what also works:
##foo = "bar"
But don't forget to restart the server after this change

Resources