How does Ruby's block syntax work? - ruby-on-rails

I'm new in Ruby and trying to understand this syntax:
create_table :posts do |t|
t.string :title
t.string :content
t.string :likes
t.string :comments
t.timestamps null: false
end
I fully understand what this code is doing, but I don't understand how it works. More specifically, I understand that create_table is a method and :posts is a parameter, but I don't understand the rest of the code.

Brace for it :)
create_table is a method. :posts is a symbol that is passed as parameter. Parenthesis are optional, so it looks odd but it is a simple method call.
Everything between do and end is a code block. It is one of the many ways how to pass code as an argument to a method. Code blocks are great for common case. Other similar (but different) ways to do it is to use Proc or lambda or ->.
|t| is an argument passed into the code block by create_table. create_table will execute your code block, and will pass a table object as single argument to it. You chose to name that object t.
Now inside your code block you are calling method string on object t and passing it symbol as an argument. You are doing it four times. Again parenthesis are optional.
You are calling timestamps method on same object t. Here you are passing it one parameter, which is a Hash with the value { :null => false }.
Not only parenthesis are optional, but also curly braces are optional when passing hash as last or only parameter to a method.
null: false, is a shortcut syntax for { :null => false }.
So all of the above is equivalent to:
create_table(:posts) do |t|
t.string(:title)
t.string(:content)
t.string(:likes)
t.string(:comments)
t.timestamps({:null => false})
end

Let's first forget about Active Record and focus on the code structure itself. Here is a super simple version of that structure.
class MyBuilder
def initialize
# keys are property names, values are options
#properties = {}
end
def property(name, options={})
#properties[name] = options
end
def build
# For simplicity, just return all properties
#properties
end
end
def create_thing(name)
puts "Begin creating #{name}"
builder = MyBuilder.new
puts "Let user use the builder to define properties"
yield builder
puts "Consume the builder"
properties = builder.build
puts "Persist changes to #{name}..."
# For simplicity just print them out
p properties
puts 'done'
end
create_thing :bar do |builder|
builder.property :counter, color: 'brown'
builder.property :wine_storage, texture: 'wood'
end
Please type the code above by hand to grab some feel.
Although the code above has nothing to do with Active Record, it has the same structure as the migration.
When ever create_table is called, it instantiates a builder (of type TableDefinition), and "pushes" that builder to the block (by yielding it) in order to let user define the tables columns. The builder is consumed later by create_table when the user is done defining the columns.

One of the struggles with learning Ruby is that, like smalltalk et al, you get to pass code around as well as data.
One way you can pass code to a method is with a code block.
You can then call the code block in the method definition with yield which says "insert this block of code in place of yield":
def do_it
yield
end
do_it { 2 + 4 }
=> 6
You also get to send parameters into the code block from the method definition.
That's where the |t| comes in:
def do_it_with_ten
yield 10
end
do_it_with_ten { |t| (2 + 4) * t }
=> 60
Note that the curly braces are equivalent to do..end.
I'm guessing that this is the code you found with yield in it:
def create_table(name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
if options[:force]
drop_table(name) rescue nil
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
This is exactly what you're looking for. This is the definition of the create_table method we are calling. You can see the yield with the parameter table_definition.

Related

Modify each block entry to call method on block argument

In one of my views, I rendered a table with a helper method, so the view (haml) looked like this:
= table do
- action "Add"
- column :id
- column :name
After I changed the helper and used the ViewComponent lib instead, I need to call it the following way:
= table do |t|
- t.action "Add"
- t.column :id
- t.column :name
I wondered if it's possible to convert the block in example 1 to the block in example 1 in a helper method, so I don't need to rewrite every view that uses a table.
The helper method would look like:
def table(*args, **kwargs, &block)
# ...
render TableComponent.new(*args, **kwargs, &new_block)
end
Let's simplify your example to this:
class Table
def initialize
#rows = []
end
def action(name)
#rows << "Action #{name}"
end
def column(name)
#rows << "Column #{name}"
end
def to_s
"Table:\n======\n#{#rows.join("\n")}"
end
end
def table(&block)
t = Table.new
block.call(t)
t
end
puts(
table do |t|
t.action "Add"
t.column :id
t.column :name
end
)
That gives:
Table:
======
Action Add
Column id
Column name
Now you want to do:
table do
action "Add"
column :id
column :name
end
So you want to have the body of the block be in the same context as the instance (e.g. like being in a Table instance), so:
def table(&block)
t = Table.new
t.instance_eval(&block)
t
end
(that's how most Domain Specific Languages are made :) )

how to change rails migration t.timestamps to use `timestamp(0) without timezone` in postgres

I'm trying to figure out how to change the native data type that t.timestamps uses in a rails migration. Default type that ends up in postgres is timestamp without timezone. What I would like instead is timestamp(0) without timezone.
I'd like to change the native data type so that when a new table is created and t.timestamps is used in the migration, it automatically creates the correct timestamp data type.
I require timestamp(0) without timezone because my rails application shares its DB with a laravel application and both applications can insert data. Due to the fact that rails uses milliseconds/laravel does not, and there doesn't seem to be a way ( as of 2018-10-23 ) for laravel to support having a table that contains different formats of timestamps (Y-m-d H:i:s.u vs Y-m-d H:i:s) without having to turn off timestamps in the model, essentially disabling the auto management of them, I'd like to have the database enforce the use of a single format(Y-m-d H:i:s).
For more details please my other question: Is there a way to change Rails default timestamps to Y-m-d H:i:s (instead of Y-m-d H:i:s.u) or have laravel ignore decimal portion of Y-m-d H:i:s.u?
So I want to use timestamp(0) to truncate the milliseconds and not have to think about setting the tables timestamp types correctly when creating a new table, as the native type would already be timestamp(0)
I've tried this
./config/environments/initializers
require "active_record/connection_adapters/postgresql_adapter"
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter
NATIVE_DATABASE_TYPES.merge!(
timestamp: { name: "timestamp(0) without timezone" }
)
end
end
end
and a migration like
class ChangeTimestampTypesToTimestamp0 < ActiveRecord::Migration[5.2]
def change
create_table :test, id: :uuid, default: -> { "gen_random_uuid()" } do|t|
t.string :name, null: false
t.timestamps
end
end
end
but that did not work.
I also tried to change the timestamp to use timestampz with the same migration above as a sanity check, still no luck...
require "active_record/connection_adapters/postgresql_adapter"
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter
NATIVE_DATABASE_TYPES.merge!(
timestamp: { name: "timestamptz" }
)
end
end
end
I believe I've figure it out!
I started looking into what NATIVE_DATABASE_TYPES were bring set by print out the variable from the console
Rails c
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES
Result:
{:primary_key=>"bigserial primary key", :string=>{:name=>"character varying"}, :text=>{:name=>"text"}, :integer=>{:name=>"integer", :limit=>4}, :float=>{:name=>"float"}, :decimal=>{:name=>"decimal"}, :datetime=>{:name=>"timestamp"}, :time=>{:name=>"time"}, :date=>{:name=>"date"}, :daterange=>{:name=>"daterange"}, :numrange=>{:name=>"numrange"}, :tsrange=>{:name=>"tsrange"}, :tstzrange=>{:name=>"tstzrange"}, :int4range=>{:name=>"int4range"}, :int8range=>{:name=>"int8range"}, :binary=>{:name=>"bytea"}, :boolean=>{:name=>"boolean"}, :xml=>{:name=>"xml"}, :tsvector=>{:name=>"tsvector"}, :hstore=>{:name=>"hstore"}, :inet=>{:name=>"inet"}, :cidr=>{:name=>"cidr"}, :macaddr=>{:name=>"macaddr"}, :uuid=>{:name=>"uuid"}, :json=>{:name=>"json"}, :jsonb=>{:name=>"jsonb"}, :ltree=>{:name=>"ltree"}, :citext=>{:name=>"citext"}, :point=>{:name=>"point"}, :line=>{:name=>"line"}, :lseg=>{:name=>"lseg"}, :box=>{:name=>"box"}, :path=>{:name=>"path"}, :polygon=>{:name=>"polygon"}, :circle=>{:name=>"circle"}, :bit=>{:name=>"bit"}, :bit_varying=>{:name=>"bit varying"}, :money=>{:name=>"money"}, :interval=>{:name=>"interval"}, :oid=>{:name=>"oid"}
turns out that timestamp was never actually set before I started including it with my
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter
NATIVE_DATABASE_TYPES.merge!(
timestamp: { name: "timestamp", limit:0 }
)
end
end
end
What was being included thought was datetime and I realized that timestamp was an alias of datetime.
I changed the NATIVE_DATABASE_TYPES merge to look like this...
require "active_record/connection_adapters/postgresql_adapter"
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter
NATIVE_DATABASE_TYPES.merge!(
datetime: { name: "timestamp", limit:0 }
)
end
end
end
I ran my migration and the columns were successfully set to timestamp(0) without timezone
Here is a solution. It alters the default timestamp precision in Rails, including for migration, for the two timestamps to one second accuracy in PostgreSQL. It's neither easy nor simple, but works for Rails 5.2 with PostgreSQL.
I think the initializer should be placed in config/initializers/ (not in environments).
Write the following file.
# ./config/initializers/arbitrary.rb
require "active_record/connection_adapters/abstract/schema_definitions.rb"
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/abstract/schema_statements"
require "active_record/connection_adapters/postgresql/schema_statements"
require "active_record/connection_adapters/postgresql_adapter"
module ActiveRecord
module ConnectionAdapters
# Overwrites a method in /abstract/schema_definitions.rb
class TableDefinition
def timestamps(**options)
options[:null] = false if options[:null].nil?
column(:created_at, :datetime0, options)
column(:updated_at, :datetime0, options)
end
end
# Overwrites a method in /abstract/schema_statements.rb
module SchemaStatements
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
add_column table_name, :created_at, :datetime0, options
add_column table_name, :updated_at, :datetime0, options
end
end
# Overwrites a method in /postgresql/schema_statements.rb
module PostgreSQL
module SchemaStatements
def add_timestamps_for_alter(table_name, options = {})
[add_column_for_alter(table_name, :created_at, :datetime0, options), add_column_for_alter(table_name, :updated_at, :datetime0, options)]
end
end
end
# Modifies a constant and methods in /postgresql_adapter.rb
class PostgreSQLAdapter
alias_method :initialize_type_map_orig, :initialize_type_map if ! self.method_defined?(:initialize_type_map_orig)
NATIVE_DATABASE_TYPES[:datetime0] = { name: "timestamp(0)" }
private
def initialize_type_map(m = type_map)
register_class_with_precision_t0 m, "timestamp0", OID::DateTime
initialize_type_map_orig(m)
end
def register_class_with_precision_t0(mapping, key, klass)
mapping.register_type(key) do |*args|
klass.new(precision: 0)
end
end
end
end
end
Here is an example migration file.
# db/migrate/20181023182238_create_articles.rb
class CreateArticles < ActiveRecord::Migration[5.2]
def change
create_table :articles do |t|
t.string :title
t.timestamps
end
end
end
Migration (bin/rails db:migrate) creates a table articles with the two timestamps columns of timestamp(0) (without timezone) in the PostgreSQL database.
The SQL executed is this:
CREATE TABLE "articles" (
"id" bigserial primary key,
"title" character varying,
"created_at" timestamp(0) NOT NULL,
"updated_at" timestamp(0) NOT NULL);
Note
I have confirmed both migration to create a table and data updating works in Rails console. It is meant to work in a migration to update a table, too, but I haven't tested it.
With a bit more tweaking it would work in other databases as well.
Basically the code above defines a new Rails type timestamp0, to which timestamps() (which is created_at and updated_at) is assigned.
If you want any other columns of timestamp to be the same (i.e., no sub-second precision in the DB), specify timestamp0 in your migration and it should work (although I haven't tested it).

How to add an extra value into a Rails param

I'm trying to solve a problem with getting all the values saved to my database. Here's how my application is setup is
before_filter :load_make, only: :create
def create
#record = #make.message.new(my_params)
#record.save
end
def load_make
make_id = params[:message] ? params[:message][:make_id] : params[:make_id]
#make = Car.find_by(custom_make: make_id)
#make ||= Car.find_by(id: make_id).tap
end
def my_params
params.require(:message).permit(:message_type, :message_date, :make_id)
end
My problem is that I want to create another method that does a GET request from a different API that returns a created_at date and also save it on create, similar to:
def lookup_car
Car.car_source.find(id: my_params[:make_id]).created_at
# returns a datetime value
end
I'm looking for advice on what's the best way to put this into the my_params method :message_date and save it along with everything else on the create action?
Schema for model:
create_table "messages", force: :cascade do |t|
t.integer "make_id"
t.string "message", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.string "message_type", limit: 255
t.datetime "message_date"
end
Firstly, you should NOT really change / update / insert to the created_at since Rails does that for you. If anything, I suggest you adding another column.
Secondly, you should NOT do and create / update on a Get request.
Other than that, adding another field to your params is easy as long as you have that column ready. Not sure how your models are structure, but here you can do something along the line like this below. Let's say you have message_date column in your whatever model, you can do this:
def my_params
message_date_param = { message_date: Time.now }
params.require(:message).permit(:message_type, :message_date, :make_id).merge(message_date)
end
I wrote this pretty late at night when I probably wasn't making much sense, but essentially what I was wanting was to update another value at same time save was called on the #record. I solved this by using attributes, which updates but doesn't call save (since I was doing that already below).
For example:
def create
car = Car.car_source.find(id: my_params[:make_id]).created_at
#record.attributes = { message_date: car }
#record.save
end
Also much thanks to all the other comments. I've completely refactored this code to be more rails idiomatic.

current_user association seems to work in view but not controller

The rundown: I have a user model which belongs_to a state model (region). The state model rb file is basically empty except for: has_many users. The db is seeded with the state info, and when a user registers they can select their region; all works fine.
Now I'm trying to use devise's current_user method in one of my controllers to check if the current_user's state_id is a match to one of those in the array.
class FinancesController < ApplicationController
def index
#user = current_user
if #user.state_id == ['53', '54', '60']
flash.now[:notice] = "User is from one of the accepted regions"
else
end
end
end
Schema: (the dots are just there to indicate that there's more than just the two lines)
create_table "users", force: true do |t|
.........
t.string "email", null: false
.....
.....
t.integer "state_id"
.....
end
Unfortunately it doesn't work, and will always use the "else" even if the user is of one of the accepted values in the array.
For example, the user I'm testing with has a state_id of 54. In my view I can use <%= current_user.state_id %> which will print "54" on the page.
I believe the problem may be with the array ['53', '54', '60'] because I tried just having one ID there instead of the array, and it seemed to produce the proper results. Perhaps I should be using a for loop to cycle through and check each number in the array if such is the case?
You are checking a string value against an array.
You would probably want to do this instead:
if [53, 54, 60].include? #user.state_id
Use include method:
['53', '54', '60'].include?(#user.state_id)

ruby object array... or hash

I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end

Resources