- historia
- el lenguaje
- interpretes
Profesor | Contacto |
---|---|
Lic. Christian A. Rodríguez |
Jefe de Trabajos Prácticos | Contacto |
---|---|
Lic. Nahuel Cuesta Luengo |
Ayudantes | Contacto |
---|---|
Emilia Corrons | |
Damián Candia |
Día | Hora | Aula |
---|---|---|
Lunes (teoría) | 18 a 20 | 14 |
Jueves (práctica) | 10 a 12 | 1 |
Dividimos el programa en secciones:
Durante la pandemia, generamos material correspondiente a este curso. Las clases pueden verse en los siguientes playlists de youtube:
A medida que se presenten los temas se indicarán las fuentes apropiadas
Los fuentes de éste material pueden encontrarse en https://github.com/ttps-ruby/ttps-ruby.github.io
Todo el material se encuentra bajo licencia Creative Commons
TTPS - Opcion Ruby por
Christian A. Rodriguez se encuentra bajo
una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.
Ruby is designed to make programmers HAPPY
Nombres válidos y convenciones:
NombreDeClaseOModulo
CONSTANTE
@nombre_de_atributo
@@atributo_de_clase
$variable_global
nombre_de_metodo
metodo_peligroso!
metodo_que_pregunta?
Todos los valores son objetos
"Aprendiendo ruby".length
"Aprendiendo ruby".each_char.sort.join
1 + 2
1.send :+, 2
["Go", "Ruby", "Java", "Python", "PHP", "Javascript"].sort
([1,2,3] + [4,5,6]).last
-100.abs
1_123_456 * 1_000_000
1.5 * 3
0b1000_1000 # Binario => 136
010 # Octal => 8
0x10 # Hexadecimal => 16
a = Array.new
a[10].nil?
nil.nil?
1.object_id
nil.object_id
'sin interpolar'
"Interpolando: #{'Ja'*3}!"
# Notación alternativa
%q/Hola/
%q!Chau!
%Q{Interpolando: #{3+3}}
un_string = <<-EOS
Este es un texto
de mas de una linea
que termina aqui.
Se puede observar que
espacios antes de cada
linea.
EOS
un_string.upcase
:action
, :line_items
, :+
:uno.object_id # siempre devolverá lo mismo
"uno".object_id # siempre devolverá diferente
['Hola', 'Chau']
# sin interpolar
%w(Hola Chau #{2+2})
# interpolando
%W(Hola Chau #{2+2})
[1,2,3,4]
# Versión 1.8
{
:nombre => 'Christian',
:apellido => 'Rodriguez'
}
# Versión > 1.8
{
nombre: 'Christian',
apellido: 'Rodriguez'
}
/^[a-zA-Z]+$/
"Do you like cats?" =~ /like/
"192.168.0.10" =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
0..1
0..10
"a".."z"
"a"..."z"
# Pueden convertirse en arreglos
("a"..."z").to_a
# Rangos como intervalos
(1..10) === 5 # => true
(1..10) === 15 # => false
(1..10) === 3.1 # => true
a = 3.14 # => 3.14
# Veamos el case
estado = nil
face = case estado
when "Feliz" then ":)"
when "Triste" then ":("
else ":|"
end
En Ruby toda expresión retorna un valor
uno = lambda { |n| n * 2 }
dos = ->(n, m){ n * 2 + m }
tres = ->(n, m=0){ n * 2 + m}
# Entonces
uno.call 2 # => 4
dos.call 2,3 # => 7
tres.call 2 # => 4
3.times do |i|
puts i
end
3.times { |i| puts i }
Rara vez usaremos un for / while
# Selección de números pares (1..10).select { |n| n.even? } # Procesar cada elemento de una colección (1..10).map { |n| n*2 } # Calcular con los elementos de la colección: (1..100).reduce { |sum,n| sum + n }
# Selección de números pares (1..10).select { |n| n.even? } # o lo que es igual: (1..10).select(& :even?) # Procesar cada elemento de una colección (1..10).map { |n| n*2 } # o lo que es igual: (1..10).collect { |n| n*2 } # Calcular con los elementos de la colección: (1..100).reduce { |sum,n| sum + n } # o lo que es igual: (1..100).reduce(:+)
File.open('/etc/passwd').each do |line|
puts line if line =~ /root/
end
Ruby como lenguaje, tiene varias implementaciones. La implementación de referencia es conocida como MRI: Matz’s Ruby Interpreter o CRuby (porque está desarrollada en C), pero existen otras implementaciones.
El uso, instalación y manejo de los entornos serán abordados en el espacio de las prácticas
"Date","ISBN","Amount"
"2008-04-12","978-1-9343561-0-4",39.45
"2008-04-13","978-1-9343561-6-6",45.67
"2008-04-14","978-1-9343560-7-4",36.95
class BookInStock
end
Recordamos que los nombres de las clases deben comenzar con mayúsculas, los métodos con minúscula
a_book = BookInStock.new
another_book = BookInStock.new
BookInStock
. class BookInStock
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
end
initialize
es especial en Ruby new
, Ruby aloca memoria para alojar un objeto no
inicializado y luego invoca al método initialize
pasándole cada parámetro
que fue enviado a new
.initialize
nos permite configurar el estado inicial de nuestros objetos.@isbn
e isbn
son diferentes.Float
toma un argumento y lo convierte a float
, terminando el programa si falla
la conversiónb1 = BookInStock.new("isbn1", 3)
p b1
b2 = BookInStock.new("isbn2", 3.14)
p b2
b3 = BookInStock.new("isbn3", "5.67")
p b3
Usamos el método
p
porque imprime el estado interno de los objetos. Si se utilizaraputs
entonces se invocaríato_s
e imprimiría:#<nombre_de_clase:id_objeto_en_hex>
class BookInStock
def to_s
"ISBN: #{@isbn}, price: #{@price}"
end
end
Probar nuevamente
puts b3
BookInStock
con el fin de agregar atributos
para isbn
y price
así podemos contabilizarlos.class BookInStock
def isbn
@isbn
end
def price
@price
end
end
A los atributos anteriores se los denomina accesor porque mapean
directamente con las variables de instancia. Ruby provee un shortcut: attr_reader
.
class BookInStock
attr_reader :isbn, :price
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
end
- Notar que se utilizan símbolos.
attr_reader
no define variables de instancia, sólo los métodos de acceso.
No sólo leemos atributos: a veces necesitamos modificar un valor. Ésto es posible definiendo un método terminado con el signo igual.
class BookInStock
attr_reader :isbn, :price
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
def price=(new_price)
@price = new_price
end
end
book = BookInStock.new("isbn1", 33.80)
book.price = book.price * 0.75 # discount price
puts "New price = #{book.price}"
Podemos usar entonces:
attr_writer
: acceso W.attr_accessor
: acceso RW.class BookInStock
attr_reader :isbn
attr_accessor :price
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
end
Agregamos el precio en centavos
class BookInStock attr_reader :isbn attr_accessor :price def initialize(isbn, price) @isbn = isbn @price = Float(price) end def price_in_cents Integer(price*100 + 0.5) end def price_in_cents=(cents) @price = cents / 100.0 end end
Ya tenemos el objeto que representa un libro. Resta implementar:
Pensamos la estructura de CsvReader
class CsvReader
def initialize
end
def read_in_csv_data(csv_file_name)
end
def total_value_in_stock
end
def number_of_each_isbn
end
end
reader = CsvReader.new
reader.read_in_csv_data("file1.csv")
reader.read_in_csv_data("file2.csv")
# Otros csv
puts "Total value in stock = #{reader.total_value_in_stock}"
CsvReader
debe ir acumulando lo que va leyendo de cada csv.require 'csv'
class CsvReader
def initialize
@books_in_stock = []
end
def read_in_csv_data(csv_file_name)
CSV.foreach(csv_file_name, headers: true) do |row|
@books_in_stock <<
BookInStock.new(row["ISBN"], row["Amount"])
end
end
end
Utilizamos la librería
csv
que nos permite acceder a los campos de cada columna por su nombre.
class CsvReader
def total_value_in_stock
sum = 0.0
@books_in_stock.each do |book|
sum += book.price
end
sum
end
end
Ya veremos una implementación más rubista que la utilizada en esta instancia.
lib/book_in_stock.rb
: la clase BookInStock
lib/csv_reader.rb
: el código de CsvReader
stock_stats.rb
: el programa principalrequire
y require_relative
require_relative 'csv_reader'
reader = CsvReader.new
ARGV.each do |csv_file_name|
STDERR.puts "Processing #{csv_file_name}"
reader.read_in_csv_data(csv_file_name)
end
puts "Total value = #{reader.total_value_in_stock}"
initialize
que es privado.self
. Esto
significa que tampoco puede invocar el método privado de otra instancia de la
misma clase.class MyClass
def method # default is public
end
protected # subsequent methods will be 'protected'
def method2
end
private # subsequent methods will be 'private'
def method3
end
public # subsequent methods will be 'public'
def method4
end
end
class MyClass
def method1; end
def method2; end
def method3; end
def method4; end
public :method1, :method4
protected :method2
private :method3
end
Is a variable an object? In Ruby, the answer is no. A variable is simply a reference to an object. Objects float around in a big pool somewhere (the heap, most of the time) and are pointed to by variables.
Analicemos el siguiente ejemplo
person1 = "Tim"
person2 = person1
person1[0] = 'J'
puts "person1 is #{person1}"
puts "person2 is #{person2}"
dup
person1 = "Tim"
person2 = person1.dup
person1[0] = 'J'
puts "person1 is #{person1}"
puts "person2 is #{person2}"
Es posible freezar objetos
person1 = "Tim"
person2 = person1
person1.freeze
person2[0] = 'J'
array
hash
o arreglo asociativoLa clase Array
mantiene una colección de referencias a objetos.
Cada referencia a objeto ocupa una posición en el arreglo, identificada por un índice entero no negativo.
a = [ 3.14159, "pie", 99 ]
a.class
a.length
a[0]
a[1]
a[2]
a[3]
b = Array.new
b.class
b.length
b[0] = "second"
b[1] = "array"
[]
[]
.[]
es un método (de instancia en la clase Array
) y por tanto puede
implementarse por cualquier subclase.nil
.a = [ 1, 7, 9]
a[-1]
a[-2]
a[-99]
a = [ 1, 3, 5, 7, 9 ]
a[1, 3]
a[3, 1]
a[-3, 2]
El significado es
[desde,cantidad]
a = [ 1, 3, 5, 7, 9]
a[1..3]
a[1...3]
a[3..3]
a[-3..-1]
Significa desde y hasta. Si se utiliza
..
se incluye el fin de rango, con...
se excluye el extremo final.
[]=
Setea elementos de un array.
a = [ 1, 3, 5, 7, 9 ]
a[1] = 'bat'
a[-3] = 'cat'
a[3] = [ 9, 8 ]
a[6] = 99
Si se utiliza un único índice, reemplaza su valor por lo que esté a la derecha de la asignación: cualquier gap que haya quedado luego de
[]=
se completa con nil.
[]=
a = [ 1, 3, 5, 7, 9 ]
a[2, 2] = 'cat'
a[2, 0] = 'dog'
a[1, 1] = [ 9, 8, 7 ]
a[0..3] = []
a[5..6] = 99, 98
stack = []
stack.push "red"
stack.push "green"
stack.push "blue"
puts stack.pop
puts stack.pop
puts stack.pop
stack = []
(stack.unshift 1).unshift 2
stack.unshift 3
puts stack.shift
puts stack.shift
puts stack.shift
array = [ 1, 2, 3, 4, 5, 6, 7 ]
p array.first(4)
p array.last(4)
h = { 'dog' => 'canine', 'cat' => 'feline' }
h.length # => 2
h['dog'] # => "canine"
h['cow'] = 'bovine'
h[12] = 'dodecine'
h['cat'] = 99
# En ruby >= 1.9
h = { dog: 'canine', cat: 'feline' }
# En ruby < 1.9
h = { :dog => 'canine', :cat => 'feline' }
Calcular el número de veces que aparece una palabra en un texto
El problema se divide en dos partes:
- Separar el texto en palabras: suena como un array
- Luego contar cada palabra diferente: suena como hash
Usando expresiones regulares y el método scan
todo parece muy simple:
def words_from_string(string)
string.downcase.scan(/[\w']+/)
end
Analizar otros ejeplos de scan. Por ejemplo probar:
"0123456789".scan /.{2}/
Con un hash indexaremos para cada palabra, la cantidad de ocurrencias.
if counts.has_key?(next_word)
counts[next_word] += 1
else
counts[next_word] = 1
end
def count_frequency(word_list)
counts = Hash.new(0)
for word in word_list
counts[word] += 1
end
counts
end
Hash.new
puede recibir como parámetro el valor usado para incializar cada valor del Hash. Ver ejemplo. Es importante destacar que el ejemplo incluye tests para analizar cómo desarrollar utilzando TDD.
do
y end
.do
/ end
.|
.Suma de los cuadrados de los números en un arreglo
sum = 0
[1, 2, 3, 4].each do |value|
square = value * value
sum += square
end
puts sum
value
sum
declarada fuera del bloque es actualizada dentro del bloquesum
square
)# assume Shape defined elsewhere
square = Shape.new(sides: 4)
#
# .. lots of code
#
sum = 0
[1, 2, 3, 4].each do |value|
square = value * value
sum += square
end
puts sum
square.draw # BOOM!
No sucede lo mismo con los argumentos al bloque
value = "some shape"
[ 1, 2 ].each {|value| puts value }
puts value
Podemos solucionar el problema de square
square = "some shape"
sum = 0
[1, 2, 3, 4].each do |value; square|
square = value * value # different variable
sum += square
end
puts sum
puts square
yield
.yield
ruby invocará al código del bloque .yield
.
def three_times
yield
yield
yield
end
three_times { puts "Hola" }
yield
podemos enviarle un parámetro
def fib_up_to(max)
i1, i2 = 1, 1
while i1 <= max
yield i1
i1, i2 = i2, i1+i2
end
end
fib_up_to(1000) {|f| print f, " " }
class Array
def my_find
for i in 0...size
value = self[i]
return value if yield(value)
end
return nil
end
end
(1..200).to_a.my_find {|x| x%5 == 0}
(1..200).to_a.my_find {|x| x == 0}
Array
hacen lo que hacen
mejor:find
), sería encontrar un elemento para el cual
el criterio sea verdadero.each
es el más simple.yield
para cada elemento.collect
también conocido como map
.yield
para cada elemento. El resultado lo guarda en un nuevo
arreglo que es retornado.[ 1, 3, 5, 7, 9 ].each {|i| puts i }
['k','h','m','t','w'].collect {|x| x.succ }
f = File.open("testfile")
f.each { |line| puts "The line is: #{line}"}
f.close
f = File.open("testfile")
f.each_with_index do |line, index|
puts "Line #{index} is: #{line}"
end
f.close
[1,3,5,7].inject(0) {|sum, element| sum+element}
[1,3,5,7].inject {|sum, element| sum+element}
[1,3,5,7].inject(1) {|prod, element| prod*element}
[1,3,5,7].inject {|prod, element| prod*element}
Un uso más críptico de inject
:
[1,3,5,7].inject(:+)
[1,3,5,7].inject 100, :+
[1,3,5,7].inject(:*)
Enumerator
.to_enum
o enum_for
.a = [ 1, 3, "cat" ]
h = { dog: "canine", fox: "lupine" }
# Create Enumerators
enum_a = a.to_enum
enum_h = h.to_enum
enum_a.next # => 1
enum_h.next # => [ :dog, "canine" ]
enum_a.next # => 3
enum_h.next # => [ :fox, "lupine" ]
Si un iterador se utiliza sin bloque, entonces retorna un Enumerator
a = [1,2,3].each
a.next
loop
terminará cuando el Enumerator se quede sin valores.
loop { puts "Hola" }
i=0
loop do
puts i += 1
break if i >= 10
end
short_enum = [1, 2, 3].to_enum
long_enum = ('a'..'z').to_enum
loop { puts "#{short_enum.next} - #{long_enum.next}" }
Sabemos que es posible usar each_with_index
en Array
result = []
[ 'a', 'b', 'c' ].each_with_index do |item, index|
result << [item, index]
end
¿Y si queremos hacer lo mismo con un String
?
each_with_index
en String
.each_char
que es como each
de Array
pero sobre cada
caracter del string.Enumerator
.Enumerable
define el método each_with_index
.result = []
"cat".each_char.each_with_index do |item, index|
result << [item, index]
end
# Aun más simple:
result = []
"cat".each_char.with_index do |item, index|
result << [item, index]
end
yield
.yield
.Generamos secuencias infinitas:
fibonacci = Enumerator.new do |caller|
i1, i2 = 1, 1
loop do
caller.yield i1
i1, i2 = i2, i1+i2
end
end
6.times { puts fibonacci.next }
Como Enumerator
es Enumerable
sería posible:
fibonacci.first(1000).last
count
y select
tratarán de
leer todos los elementos antes de retornar un valor.select
adecuada a nuestra lista
infinita.def infinite_select(enum, &block)
Enumerator.new do |caller|
enum.each do |value|
caller.yield(value) if block.call(value)
end
end
end
p infinite_select(fibonacci) {|val| val % 2 == 0}.first(5)
Podemos escribir filtros como infinite_select
directamente en la clase
Enumerator
class Enumerator
def infinite_select(&block)
Enumerator.new do |caller|
self.each do |value|
caller.yield(value) if block.call(value)
end
end
end
end
p fibonacci.
infinite_select {|val| val % 2 == 0}.
infinite_select {|val| val.to_s =~ /13\d$/ }.
first(2)
class File
def self.open_and_process(*args)
f = File.open(*args)
yield f
f.close()
end
end
File.open
.File.open
.*args
que significa:
tomar todos los argumentos enviados al método actual y colocarlos en un
arreglo llamado args.File.open(*args)
. Utilizar *args vuelve a expandir los
elementos del arreglo a parámetros individuales.class File
def self.my_open(*args)
result = file = File.new(*args)
if block_given?
result = yield file
file.close
end
return result
end
end
Esta técnica es tan útil, que
File.open
ya lo implementa. Además de usarFile.open
para abrir un archivo, podemos usarlo para directamente procesarlo como lo hacíamos conopen_and_process
.
class ProcExample
def pass_in_block(&action)
@stored_proc = action
end
def use_proc(parameter)
@stored_proc.call(parameter)
end
end
eg = ProcExample.new
eg.pass_in_block { |param| puts "The parameter is #{param}" }
eg.use_proc(99)
Notar que pasando
&action
podemos almacenar el bloque en una variable. Si no se usara&
no sería posible.
call
invoca la ejecución del bloque.def create_block_object(&block)
block
end
bo = create_block_object do |param|
puts "You called me with #{param}"
end
bo.call 99
bo.call "cat"
lamda
y Proc.new
toman un bloque y retornan un objeto.Proc
.Ya hemos mencionado que
lambda
controla los parámetros que requiere el bloque mientras queProc
no lo hace.
Los bloques pueden utilizar variables que están dentro del alcance del bloque.
def n_times(thing)
lambda {|n| thing * n }
end
p1 = n_times(10)
p1.call(3)
p1.call(4)
p2 = n_times("Hola ")
p2.call(3)
¿Qué hace el ejemplo anterior?
n_times
referencia un parámetro thing
que es usado dentro el bloque.call
el parámetro thing
está fuera del alcance,
el parámetro se mantiene accesible dentro del bloque.Closure: variables en el alcance cercano que son referenciadas por el bloque se mantienen accesibles por la vida del bloque y la vida del objeto Proc creado para este bloque.
def what_do_i_do?
value = 1
lambda { value += value }
end
let_me_see = what_do_i_do?
let_me_see.call
let_me_see.call
let multiplicar = ( x => (y) => x*y )
multiplicar(2)(3)
lambda { |params| ... }
# es equivalente a
->params { ... }
# Y con parámetros
proc1 = -> arg {puts "proc1:#{arg}" }
proc2 = -> arg1, arg2 {puts "proc2:#{arg1} y #{arg2}" }
proc3 = ->(arg1, arg2) {puts "proc3:#{arg1} y #{arg2}" }
proc1.call "ant"
proc2.call "bee", "cat"
proc3.call "dog", "elk"
Reimplementamos un while usando bloques
def my_while(cond, &body)
while cond.call
body.call
end
end
a = 0
my_while(Proc.new { a < 3 }) do
puts a
a += 1
end
def my_while(cond, &body)
while cond.call
body.call
end
end
a = 0
my_while -> { a < 3 } do
puts a
a += 1
end
&
).proc1 = lambda do |a, *b, &block|
puts "a = #{a.inspect}"
puts "b = #{b.inspect}"
block.call
end
proc1.call(1, 2, 3, 4) { puts "in block1" }
proc2 = -> a, *b, &block do
puts "a = #{a.inspect}"
puts "b = #{b.inspect}"
block.call
end
proc2.call(1, 2, 3, 4) { puts "in block2" }
Más adelante veremos en detalle cómo funciona el splat.
Para entender por qué funciona:
[1,2,3].inject &:+
Analizando qué es lo que sucede en el siguiente ejemplo
o = Object.new
[1,2,3].inject &o
# TypeError: wrong argument type Object (expected Proc)
Una solución es implementar to_proc
:
class Object
def to_proc
Proc.new {}
end
end
o = Object.new
[1,2,3].inject &o
inject
.Analizando entonces lo que sucedió inferimos que la clase Symbol
implementa
#to_proc
de la siguiente forma:
class Symbol
def to_proc
lambda { |obj| obj.send(self) }
end
end
[1,2,3].map &:to_s
[1,2,3].inject &:+
¡¡Probemos!!
Funciona map
pero no inject
. Observemos bien el error.
class Symbol
def to_proc
lambda do |obj, args|
obj.send(self, *args)
end
end
end
[1,2,3].map &:to_s
[1,2,3].inject &:+
¡¡Probemos!!
Funciona inject
pero no map
. Observemos bien el error.
Claro ejemplo de solucionar algo y romper otra cosa. Caso que se controla utilizando tests de unidad.
La solución a ambos problemas:
class Symbol
def to_proc
lambda { |obj, args=nil| obj.send(self, *args) }
end
end
[1,2,3].map &:to_s
[1,2,3].inject &:+
class Parent
def say_hello
puts "Hello from #{self}"
end
end
p = Parent.new
p.say_hello
class Child < Parent
end
c = Child.new
c.say_hello
El método superclass
devuelve la clase padre
puts "The superclass of Child is #{Child.superclass}"
puts "The superclass of Parent is #{Parent.superclass}"
puts "The superclass of Object is #{Object.superclass}"
- Si no se define superclase, Ruby asume
Object
to_s
está definido aquíBasicObject
es utilizado en metaprogramación.- Su padre es
nil
- Es la raíz: todas las clases lo tendran como ancestro
GServer
es un servidor TCP/IP genérico./var/log/syslog
serve
.serve
.require 'gserver'
class LogServer < GServer
def initialize
super(12345)
end
def serve(client)
log "Connected from #{remote_ip[2]}:#{remote_ip[1]}"
client.puts get_end_of_log_file
end
private
def get_end_of_log_file
File.open("/var/log/syslog") do |log|
# back up 1000 characters from end
log.seek(-1000, IO::SEEK_END)
# ignore partial line
log.gets
# and return rest
log.read
end
end
end
server = LogServer.new
server.start.join
GServer debe instalarse como una librería externa. Ver ejemplo
LogServer
hereda de GServer
.initialize
super
.serve
- Veremos más adelante que esta práctica muy común en OO no la convierte en un buen diseño
- En su lugar veremos mixins
- Pero para explicar mixins, antes tenemos que explicar módulos
module Trig
PI = 3.141592654
def self.sin(x)
# ..
end
def self.cos(x)
# ..
end
end
module Moral
VERY_BAD = 0
BAD = 1
def self.sin(badness)
# ...
end
end
y = Trig.sin(Trig::PI/4)
wrongdoing = Moral.sin(Moral::VERY_BAD)
- Así como en los métodos de clase, se invocan los métodos de un módulo precediéndolos con el nombre del módulo y un punto.
- Las constantes se referencian con el nombre del módulo y doble dos puntos (::).
self.cos
.
module Debug
def who_am_i?
"#{self.class.name}(\##{self.object_id}):#{self.to_s}"
end
end
class Phonograph
include Debug
def initialize(n); @n=n; end
def to_s; @n; end
end
class EightTrack
include Debug
def initialize(n); @n=n; end
def to_s; @n; end
end
ph = Phonograph.new("West End Blues")
et = EightTrack.new("Surrealistic Pillow")
ph.who_am_i?
et.who_am_i?
include
en Ruby agrega una referencia al módulo que agregará nuevos
métodos a nuestra clase.Comparable
:<
, <=
, ==
, >=
, >
.between?
.<=>
.Person
class Person
include Comparable
attr_reader :name
def initialize(name)
@name = name
end
def to_s
"#{@name}"
end
def <=>(other)
self.name <=> other.name
end
end
p1 = Person.new("Matz")
p2 = Person.new("Guido")
p3 = Person.new("Larry")
[p1, p2, p3].sort
Enumerable
each
, include?
,
find_all?
, map
, inject
, count
, etc.Enumerable
.each
.<=>
entonces
dispondremos de: min
max
sort
Creamos nuestra clase Enumerable
class VowelFinder
include Enumerable
def initialize(string)
@string = string
end
def each
@string.scan(/[aeiou]/i) do |vowel|
yield vowel
end
end
end
vf = VowelFinder.new "El murcielago tiene todas"
vf.inject(:+)
Ahora nuestra clase funciona igual que otras colecciones:
[ 1, 2, 3, 4, 5 ].inject(:+)
( 'a'..'m').inject(:+)
module Summable
def sum
inject(:+)
end
end
Lo aplicamos a las clases del ejemplo
class Array; include Summable; end
class Range; include Summable; end
class VowelFinder; include Summable; end
[ 1, 2, 3, 4, 5 ].sum
('a'..'m').sum
vf.sum
module Observable
def observers
@observer_list ||= []
end
def add_observer(obj)
observers << obj
end
def notify_observers
observers.each {|o| o.update }
end
end
- En ruby las variables de instancia se crean cuando se nombran por primera vez.
- Esto significa que un Mixin podrá crear variables de instancia si las nombra por primera vez en la clase.
¿Cómo se resuelve el nombre de un método que es el mismo en la clase, que es implementado en la superclase y además definido en uno o varios módulos incluidos?
module MyModule
def test
"Module"
end
end
class Parent
def test
"Parent"
end
end
class Child < Parent
include MyModule
def test
"Child"
end
end
t = Child.new
p t.test
module MyModule
def test
"Module"
end
end
class Parent
def test
"Parent"
end
end
class Child < Parent
include MyModule
end
t = Child.new
p t.test
module MyModule
def test1
"Module"
end
end
class Parent
def test
"Parent"
end
end
class Child < Parent
include MyModule
end
t = Child.new
p t.test
Si analizamos la salida de #ancestors
veremos la cadena Clases por la
que se buscará por un método apropiado. Veamos el siguiente ejemplo:
module Logging
def log(level, message)
puts "#{level}: #{message}"
end
end
class Service
include Logging
end
Service.ancestors
La salida es:
[Service, Logging, Object, Kernel, BasicObject]
Notar que Logging se interpone entre Object y Service
Si agregamos otro Modulo a la clase anterior:
Service.include Comparable
Podemos verificar que el último módulo incluido aparece detrás de la clase
Service
, explicando así las precedencias explicadas como casos anteriormente.
[Service, Comparable, Logging, Object, Kernel, BasicObject]
Utilizar #extend
en una clase importará los métodos del módulo como métodos de
clase.
En vez de actualizar la lista de ancestros, #extend
modificará el singleton de
la clase extendida, agregando métodos de clase.
En general, se utilizará #include
en una clase para extender el comportamiento
con métodos de instancia, pero a su vez podría necesitarse usar #extend
para
extender los métodos de clase. Entonces se necesitarían dos modulos diferentes
para cada caso.
La siguiente estrategia permite crear dos módulos para extender clases y objetos en un mismo código:
module Logging
module ClassMethods
def logging_enabled?
true
end
end
def self.included(base)
base.extend(ClassMethods)
end
def log(level, message)
puts "#{level}: #{message}"
end
end
Usando el ejemplo anterior, al realizar:
String.include Logging
String.logging_enabled?
'Test'.log 'ERROR', 'test message'
Herencia y Mixins ambos permiten escribir código en un único lugar.
¿Cuándo usar cada uno?
Una persona no es un DatabaseWrapper. Una Persona usa un DatabaseWrapper para persistirla.
Exception
se propagará hacia arriba en la pila de ejecución hasta
que el sistema detecte código que sepa manejar dicha excepción.Exception
.Exception
o con una clase propia.Analizamos el siguiente código
require 'open-uri'
web_page = URI.open("https://www.ruby-lang.org/en/documentation/")
output = File.open("ruby.html", "w")
while line = web_page.gets
output.puts line
end
output.close
Agregamos el manejador de excepción
require 'open-uri'
page = "unlp"
file_name = "#{page}.html"
output = File.open(file_name, "w")
begin
web_page = URI.open("https://www.ruby-lang.org/en/#{page}")
while line = web_page.gets
output.puts line
end
output.close
rescue Exception
STDERR.puts "Failed to download #{page}: #{$!}"
output.close
File.delete(file_name)
raise
end
$!
.raise
sin parámetros, que
relanza la excepción en $!
.Exception
StandardError
ArgumentError
FiberError (1.9)
IndexError
KeyError (1.9)
StopIteration (1.9)
IOError
EOFError
LocalJumpError
NameError
NoMethodError
Exception
StandardError
RangeError
FloatDomainError
RegexpError
RuntimeError
SystemCallError
(Errno::xxx)
ThreadError
TypeError
ZeroDivisionError
fatal
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SecurityError
SignalException
Interrupt
SystemExit
SystemStackError
rescue
para un bloque.rescue
puede indicar varias excepciones a catchear.rescue
, podemos indicar el nombre de la variable que
usaremos para mapear la exepción (en vez de usar $!
)def my_eval(string)
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom.message
rescue StandardError => bang
print "Error running script: " + bang.message
end
end
my_eval 'Float 2,2'
my_eval 'undefined_method'
rescue
utilizar, es similar al caso de un case
.rescue
compara la excepción lanzada con cada uno de los parámetros
nombrados:parámetro == $!
StandardError
.begin/end
buscando en el
método que invocó un manejador para la misma, y así siguiendo hacia arriba en
la pila.rescue
, pero
podemos usar expresiones que retornen una subclase de Exception
.ensure
se ejecutará siempre, haya sido una ejecución exitosa
o con algún problema.f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close
end
else
aplica cuando ninguno de los rescue
manejan la excepción.ensure
ejecutará siempre, incluso cuando no se produce
un error.f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
else
puts "Congratulations-- no errors!"
ensure
f.close
end
retry
para volver a ejecutar el bloque
begin/end
.@esmtp = true
begin
# First try an extended login. If it fails
# because the server doesn't support it,
# fall back to a normal login
if @esmtp then
@command.ehlo(helodom)
else
@command.helo(helodom)
end
rescue ProtocolError
if @esmtp then
@esmtp = false
retry
else
raise
end
end
Podemos lanzar excepciones usando el método Kernel.raise
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
RuntimeError
si no.
Usualemnte dentro de rescue
para el primer caso.RuntimeError
con el mensaje indicado.Kernel.caller
genera la traza de ejecución.raise
raise "Missing name" if name.nil?
if i >= names.size
raise IndexError, "#{i} >= size (#{names.size})"
end
raise ArgumentError, "Name too big", caller
Generalmente no se incluye la traza en librerías
throw(symbol, variable)
.En el siguiente ejemplo es importante que el último puts retorna
nil
def only_words(filename)
word_list = File.open(filename)
word_in_error = catch(:done) do
result = []
while line = word_list.gets
word = line.chomp
throw(:done, word) unless word =~ /^\w+$/
result << word
end
puts result.reverse
end
if word_in_error
puts "Failed: '#{word_in_error}' found. Not a word"
end
end
Fixnum
en el rango de (-2^30..2^30-1 o -2^62..2^62-1)
.Integer
.'1' + '2' => '12'
.1 + 2 # => 3
1 + 2.0 # => 3.0
1.0 + 2 # => 3.0
1.0 + Complex(1,2) # => (2.0,2i)
1 + Rational(2,3) # => (5/3)
1.0 + Rational(2,3) # => 1.66666666666665
# Y cuando se divide:
1.0/2 # => 0.5
1/2.0 # => 0.5
1/2 # => 0
Probar la división requiriendo mathn
. A partir de ruby 2.5 debe instalrse
como gema: gem install cmath mathn
US-ASCII
en
1.9 y UTF-8
a partir de ruby 2.Cambiamos la codificación de un fuente agregando en la primer línea un
comentario #encoding: xxxx
.
#encoding: iso-8859-1
txt = "dog"
puts "Encoding of #{txt.inspect} is #{txt.encoding}"
Otro uso rangos en condiciones permite al objeto rango mantener el estado de las comparaciones que macheen desde un valor (el inicial del rango) hasta el final (del rango).
100.times {|x| p x if x==50 .. x==55 }
while line = gets
puts line if line =~ /start/ .. line =~ /end/
end
car_age = gets.to_f # let's assume it's 5.2
case car_age
when 0...1
puts "Mmm.. new car smell"
when 1...3
puts "Nice and new"
when 3...6
puts "Reliable but slightly dinged"
when 6...10
puts "Can be a struggle"
when 10...30
puts "Clunker"
else
puts "Vintage gem"
end
car_age = gets.to_f # let's assume it's 5.2
case car_age
when 0..0
puts "Mmm.. new car smell"
when 1..2
puts "Nice and new"
when 3..5
puts "Reliable but slightly dinged"
when 6..9
puts "Can be a struggle"
when 10..29
puts "Clunker"
else
puts "Vintage gem"
end
def String; 'string'; end;
String() vs String
?
!
=
def concat(a="a", b="b")
"#{a},#{b}"
end
def surround(word, pad_width=word.length/2)
"[" * pad_width + word + "]" * pad_width
end
*
antes del nombre del argumento y luego de los parámetros normales.def varargs(arg1, *rest)
"arg1=#{arg1}. rest=#{rest.inspect}"
end
super
.super
sin argumentos invoca el método del padre con todos los argumentos recibidos.class Child < Parent
def do_something(*not_used)
# our processing
super
end
end
class Child < Parent
def do_something(*)
# our processing
super
end
end
return
para forzar la salida.return
se retorna un arreglo.Es la idea inversa a la explicación de splat previa
def five(a, b, c, d, e)
"I was passed #{a} #{b} #{c} #{d} #{e}"
end
five(1, 2, 3, 4, 5 )
five(1, 2, 3, *['a', 'b'])
five(*['a', 'b'], 1, 2, 3)
five(*(10..14))
five(*[1,2], 3, *(4..5))
Al igual que en el caso anterior de splat, podemos necesitar especificar que uno de los parámetros a un método es un bloque
## En vez de
(1..10).collect { |x| x*2}.join(',')
## Podemos usar
b = -> x { x*2}
(1..10).collect(&b).join ','
class SongList
def search(name, params)
# ...
end
end
list.search(:titles,
{ :genre => "jazz",
:duration_less_than => 270
})
El primer parámetro indica qué atributo retornar mientras que el segundo es un hash con el criterio de búsqueda.
{}
, además de la posible
confusión con la posibilidad de que se esté indicando un bloqueclave => valor
en la lista de
argumentos siempre que:# Ruby <= 1.9
list.search(:titles,
:genre => 'jazz',
:duration_less_than => 270)
# Ruby >= 1.9
list.search(:titles, genre: 'jazz', duration_less_than: 270)
Solo en Ruby 2.0 o superior.
Asumimos un supuesto método log
Keyword arguments
def log(msg, level: "ERROR", time: Time.now)
puts "#{ time.ctime } [#{ level }] #{ msg }"
end
Usando hash
def log(msg, opt = {})
level = opt[:level] || "ERROR"
time = opt[:time] || Time.now
puts "#{ time.ctime } [#{ level }] #{ msg }"
end
log("Hello!", level: "INFO")
Simular keyword arguments con hash:
def log(*msgs)
opt = msgs.last.is_a?(Hash) ? msgs.pop : {}
level = opt.key?(:level) ? opt.delete(:level) : "ERROR"
time = opt.key?(:time ) ? opt.delete(:time ) : Time.now
raise "unknown keyword: #{ opt.keys.first }" if !opt.empty?
msgs.each {|msg| puts "#{ time.ctime } [#{ level }] #{ msg }" }
end
Pero nos gustó preservar la primer versión del ejemplo
Probamos los argumentos
log("Hello")
log("Hello!", level: "ERROR", time: Time.now)
Y si cambiamos el orden
log("Hello!", time: Time.now, level: "ERROR")
Cuando enviamos un argumento no conocido
log("Hello!", date: Time.new)
Si queremos evitar las excepciones podemos usar **
para explícitamente
agrupar el resto de los keyword arguments en un hash (como splat).
def log(msg, level: "ERROR", time: Time.now, **kwrest)
puts "#{ time.ctime } [#{ level }] #{ msg }"
end
log("Hello!", date: Time.now)
Una función que considera todos los casos:
def f(a, b, c, m = 1, n = 1, *rest, x, y, z, k: 1, **kwrest, &blk)
puts "a: %p" % a
puts "b: %p" % b
puts "c: %p" % c
puts "m: %p" % m
puts "n: %p" % n
puts "rest: [%p]" % rest.join(',')
puts "x: %p" % x
puts "y: %p" % y
puts "z: %p" % z
puts "k: %p" % k
puts "kwrest: %p" % kwrest
puts "blk: %p" % blk
end
f("a", "b", "c", 2, 3, "foo", "bar", "baz", "x",
"y", "z", k: 42, u: "unknown") { }
+
, -
, *
, /
, etca, b, c = 1, 2, 3
a * b + c
# O en forma similar
(a.*(b)).+(c)
class Integer
alias old_plus +
def +(other)
old_plus(other).succ
end
end
<<
class ScoreKeeper
def initialize
@total_score = 0
@count = 0
end
def <<(score)
@total_score += score
@count += 1
self
end
def average
fail "No scores" if @count == 0
Float(@total_score) / @count
end
end
scores = ScoreKeeper.new
scores << 10 << 20 << 40
puts "Average = #{scores.average}"
[]
class SomeClass
def []=(*params)
value = params.pop
puts "Indexed with #{params.join(', ')}"
puts "value = #{value.inspect}"
end
end
s = SomeClass.new
s[1] = 2
s['cat', 'dog'] = 'enemies'
Podemos usar comillas: `** ó **%x
para indicar la ejecución de un comando
en el sistema operativo subyacente:
`date`
`ls /bin`.split[34]
%x{echo "Hello there"}
`ip address ls`.
split("\n").
select {|x| x =~ / inet / }.
map do |x|
x.scan(/((\d{1,3}\.?){4}\/(\d){1,2})/).flatten.shift
end
Jugando con splat y asignación en paralelo
a, b, c, d, e = *(1..2), 3, *[4, 5] # a=1, b=2, c=3, d=4, e=5
a1, *b1 = 1, 2, 3 # a1=1, b1=[2, 3]
a2, *b2 = 1 # a2=1, b2=[]
*a3, b3 = 1, 2, 3, 4 # a3=[1, 2, 3], b3=4
c, *d, e = 1, 2, 3, 4 # c=1, d=[2,3], e=4
f, *g, h, i, j = 1, 2, 3, 4 # f=1, g=[], h=2, i=3, j=4
El operador &&
y el método and
funcionan similar: retornan el
primer valor si es falso, sino el segundo.
nil && 99 # => nil
false && 99 # => false
"cat" && 99 # => 99
a = (true and false)
a = true and false # Check a, Why??
Ambos son iguales salvo por la precedencia: and
es de menor precedencia
que &&
.
El operador ||
y el método or
funcionan similar: retornan el
primer valor si es verdadero, sino el segundo.
nil || 99 # => 99
false || 99 # => 99
"cat" || 99 # => "cat"
b = (false or true)
b = false or true # Check b, Why??
or
es de menor precedencia
que ||
.
Es muy común utilizar la expresión: ||=
para setear un valor si no fue
seteado:
var ||= "default value"
break
: termina en forma inmediata al loop que se encuentra más próximo. El
control se devuelve a la sentencia siguiente al final del bloqueredo
: repite la iteración actual sin evaluar la condición ni trayendo el
siguiente elemento si fuese un iteradornext
: avanza hasta el final del bloque continuando con la siguiente
iteracióna = 0
while a < 20 do
a +=1
break if a == 10
p a
end
a = 0
while a < 20 do
a +=1
redo if a == 10
p a
end
a = 0
while a < 20 do
a +=1
redo if a == 20
p a
end
a = 0
while a < 20 do
a +=1
next if a == 10
p a
end
a = 0
while a < 20 do
a +=1
next if a == 20
p a
end
gem install rake
gem search sinatra
gem list
gem install bundler
Bundler es análogo a composer para PHP, Pipenv para Python, maven para java, npm o yarn para Javascript. El manejo de dependencias debe realizarse según el segundo factor de 12-factor apps.
Definimos las dependencias en el archivo Gemfile
source 'https://rubygems.org'
gem 'sinatra'
Luego instalamos las dependencias con bundle install
o simplemente bundle
.
# Instalar dependencias:
bundle install
# Actualizar dependencias a sus últimas versiones:
bundle update
# Ejecutar un script en el contexto del bundle actual:
bundle exec
# Ver las gemas instaladas en el bundle actual:
bundle list
# Ver donde está ubicada una gema:
bundle show NOMBRE_GEMA
gem
indica una dependencia y acepta los siguientes
parámetros:'>= 1.1.0'
, '~> 3.1.2'
github: 'sinatra/sinatra'
source 'https://rubygems.org'
gem 'sinatra', github: 'sinatra/sinatra'
gem 'activerecord', '~> 3.1.0'
Con declarar las dependencias en el Gemfile
no basta, hay que invocar a
bundler desde el código.
require 'bundler'
Bundler.require
require 'bundler'
Bundler.setup
require 'sinatra'
Es posible definir la fuente de donde obtener las gemas:
source 'https://rubygems.org'
Incluso es posible hacerlo por gema o grupo de gemas
Es posible también indicar cómo sea require una gema:
gem "redis", :require => ["redis/connection/hiredis", "redis"]
gem "webmock", :require => false
Y por supuesto es posible definir la Versión de una gema:
gem 'rails', '5.0.0'
gem 'rack', '>=1.0'
gem 'thin', '~>1.1'
source 'https://rubygems.org'
gem 'thin', '~>1.1'
gem 'rspec', :require => 'spec'
gem 'my_gem', '1.0', :source => 'https://gems.example.com'
gem 'mysql2', platform: :ruby
gem 'jdbc-mysql', platform: :jruby
gem 'activerecord-jdbc-adapter', platform: :jruby
source 'https://gems.example.com' do
gem 'another_gem', '1.2.1'
end
gem 'nokogiri',
:git => 'https://github.com/tenderlove/nokogiri.git',
:branch => '1.4'
gem 'extracted_library', :path => './vendor/extracted_library'
gem 'wirble', :group => :development
gem 'debugger', :group => [:development, :test]
group :test do
gem 'rspec'
end
Conventioin over Configuration
Active Record mapea automáticamente entre tablas y clases, atributos y columnas:
class Product < ActiveRecord::Base
end
La clase Product
se mapea automáticamente a la tabla llamada products
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
Además se definen accessors para cada campo. En el ejemplo serían name
y
name=
.
Los nombres de las clases se pluralizan para encontrar las tablas
Book
se mapea a books
BookClub
se mapeará con
la tabla book_clubs
.Las definiciones de estos mapeos son mediante otra gema llamda
ActiveSupport
.
Modelo / Clase | Tabla / Schema |
---|---|
Post | posts |
LineItem | line_items |
Deer | deers |
Mouse | mice |
Person | people |
irb -r active_support/all
ActiveSupport::Inflector.pluralize 'person'
Active Record utiliza convenciones para las columnas de las tablas, dependiendo del propósito de estas columnas.
nombre_en_singular_id
(por ejemplo: item_id
, order_id
). Estos serán
los campos por los que AR buscará cuando se creen asociaciones entre
modelos.id
como clave primaria. Cuando se usan Migraciones de
AR para crear las tablas, esta columna se creará automáticamente.created_at
: esta columna automáticamente setea la fecha y hora cuando el
registro es creado.updated_at
: esta columna automáticamente setea la fecha y hora cuando el
registro es actualizado.lock_version
: agrega optimistic
locking al modelotype
: especifica que el modelo utiliza Single Table
Inheritance.(association_name)_type
: especifica el tipo de asociaciones
polimórifcas.(table_name_plural)_count
: usado para cachear el número de registros que pertenecen
a una asociación. Por ejemplo, una columna comments_count
en la clase Post
que tiene muchas instancias de Comment
, cacheará el número de comentarios existentes para cada post.type
.ActiveRecord::Base.logger
asociacion_en_plural_count
y agregando un modificador
a la asociación: belongs_to
llamado counter_cache: true
CRUD significa Create, Read, Update, Delete.
En criollo solemos decirle ABM, aunque nos falta una letra para que sea completa la comparación.
new
retornará un objeto nuevo mientras que create
retornará
un objeto y lo guardará en la base de datosCreamos usando un hash:
user = User.create(name: "David",
occupation: "Code Artist")
Es lo mismo que hacerlo con atributos:
user = User.new
user.name = "David"
user.occupation = "Code Artist"
user.save
Creación con bloques
user = User.new do |u|
u.name = "David"
u.occupation = "Code Artist"
end
funciona tanto con
new
comocreate
.
# return a collection with all users
users = User.all
# return the first user
user = User.first
# find all users named David who are Code Artists and
# sort by created_at inreverse chronological order
users = User.where(name: 'David',
occupation: 'Code Artist').
order('created_at DESC')
Una vez que un dato es recuperado, sus atributos pueden modificarse y luego almacenarse en la base de datos nuevamente
user = User.find_by(name: 'David')
user.name = 'Dave'
user.save
# Lo mismo pero más corto
user = User.find_by(name: 'David')
user.update(name: 'Dave')
# Para cambios masivos
User.update_all "max_attempts = 3, must_change_pwd = 'true'"
De igual forma, una vez recuperado un objeto Active Record, podrá destruirse y a su vez eliminarse de la base de datos
user = User.find_by(name: 'David')
user.destroy
create
, save
y update
usan validaciones.false
cuando la validación falla y no actualizan el dato en la
base de datos.bang!
(create!
,
save!
y update!
) que son estrictos en cuanto a lanzar una
excepción ActiveRecord::RecordInvalid
cuando la validación falla.class User < ActiveRecord::Base
validates :name, presence: true
end
User.create
# => User not persisted
User.create!
# => ActiveRecord::RecordInvalid:
# Validation failed:
# Name can't be blank
rake
Ejemplo de una migración que crea una tabla
class CreatePublications < ActiveRecord::Migration
def change
create_table :publications do |t|
t.string :title
t.text :description
t.references :publication_type
t.integer :publisher_id
t.string :publisher_type
t.boolean :single_issue
t.timestamps
end
add_index :publications, :publication_type_id
end
end
rake db:migrate
rake db:rollback
Otros productos que ofrecen alternativas son:
Garantizan que se guarden datos válidos en la base de datos.
Utilizar validaciones en la propia base o usar store procedures dificultan la portabilidad de la aplicación a otros motores. Además no es simple realizar los tests de la aplicación. Sin embargo, no es una mala práctica aplicar restricciones en la base de datos como complemento.
Validar del lado del cliente usando por ejemplo javascript. Esta funcionalidad no garantiza consistencia dado que podrían enviarse datos no validados de forma intencional.
Validaciones en el controlador podría ser otra alternativa. Sin embargo el testeo de los controladores se complejizaría. Por otra parte es una buena idea mantener los controladores bien delgados
.new
hasta que no se les diga save
Exite el método .new_record?
que indica la situación de un objeto
Person = Class.new(ActiveRecord::Base)
p = Person.new(name: "John Doe")
p.new_record?
p.save
p.new_record?
INSERT
.UPDATE
:Antes de estas acciones, se realizan las validaciones. Si alguna de las validaciones falla entonces el objeto será marcado como inválido y ActiveRecord no realizará la acción.
Hay muchas formas de cambiar el estado de un objeto en la DB. Algunos métodos realizarán validaciones, pero otros no, significando que podrían guardarse datos inválidos en la DB.
Métodos que realizan validaciones
create
create!
save # Puede recibir validate: false
save!
update
update!
Métodos que NO realizan validaciones
decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters
Independientemente de los métodos antes mencionados que lanzan
validaciones, puede utilizarse valid?
e invalid?
para lanzar
validaciones:
class Person < ActiveRecord::Base
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
errors.messages
, una colección de errores indexada por el campo con errores.new
que técnicamente es erróneo, no muestra
errores porque aún no se han corrido las validaciones.class Person < ActiveRecord::Base
validates :terms_of_service, acceptance: true
end
class Library < ActiveRecord::Base
has_many :books
validates_associated :books
end
_confirmation
._confirmation
no es nil, por lo que
debe asegurarse su existencia.class Library < ActiveRecord::Base
validates :email, confirmation: true
validates :email_confirmation, presence: true
end
Se utilizan para validar la (ex/in)clusión de valores admisibles:
class Library < ActiveRecord::Base
validates :subdomain,
exclusion: { in: %w(www us ca jp)
end
class Coffee < ActiveRecord::Base
validates :size,
inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }
end
Validan el formato con una expresión regular que se especifica usando la
opción with:
class Product < ActiveRecord::Base
validates :legacy_code, format: {
with: /\A[a-zA-Z]+\z/,
message: "only allows letters" }
end
Valida la longitud de un campo de diversas formas:
class Person < ActiveRecord::Base
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
only_integer
.:greater_than
,
:greater_than_or_equal_to
, :equal_to
, :less_than
,
:less_than_or_equal_to
, :odd
, :even
class Player < ActiveRecord::Base
validates :points, numericality: true
validates :games_played,
numericality: { only_integer: true }
end
Valida que el atributo esté o no presente (esté vacío) usando blank?
para
verificar si un valor es nil
o un string blanco (esto es vacío o consiste de
espacios). Incluso permite validar que una asociación esté presente.
class Person < ActiveRecord::Base
validates :name, :login, :email, presence: true
end
# Es importante para usar el siguiente ejemplo que la
# asociación use inverse_of
class LineItem < ActiveRecord::Base
belongs_to :order
validates :order, presence: true
end
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
false.blank?
es true
, hay que tener especial cuidado con campos
booleanos.nil
.validates :boolean_field_name, inclusion: { in: [true, false] }
validates :boolean_field_name, exclusion: { in: [nil] }
Para el caso de
absence
es necesario algo como:validates :field_name, exclusion: { in: [true, false]
considerando quefalse.present?
devuelvefalse
class Account < ActiveRecord::Base
validates :email, uniqueness: true
end
class Holiday < ActiveRecord::Base
validates :name, uniqueness: {
scope: :year,
message: "should happen once per year" }
end
class Person < ActiveRecord::Base
validates :name,
uniqueness: { case_sensitive: false }
end
Existe la posibilidad de explicitar un alcance de la unicidad de forma de combinar con otros campos. Es posible especificar otra opción
case_sesitive
para verificar la unicidad considerando este factor o no.
:if
y :unless
que reciben:
Proc
, un string o arreglo.validates :surname, presence: true, if: "name.nil?"
if: ["market.retail?", :desktop?]
Las asociaciones simplifican la interacción entre modelos relacionados
class Customer < ActiveRecord::Base
end
class Order < ActiveRecord::Base
end
¿Cómo podemos representar la relación entre clientes que tienen varias ordenes?
Si los clientes pueden tener varias órdenes la forma de relacionarlos sin utilizar asociaciones sería:
@order = Order.create order_date: Time.now,
customer_id: @customer.id
# Para eliminar un cliente con sus ordenes:
@orders = Order.where customer_id: @customer.id
@orders.each do |order|
order.destroy
end
@customer.destroy
class Customer < ActiveRecord::Base
has_many :orders, dependent: :destroy
end
class Order < ActiveRecord::Base
belongs_to :customer
end
# Crear una orden:
@order = @customer.orders.create order_date: Time.now
# Eliminar un cliente:
@customer.destroy
belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many
has_many
o has_one
desde
el otro modelo.Clientes con múltiples órdenes, donde cada orden es de un cliente.
Es importante destacar que estas asociaciones deben usar términos en singular.
En el ejemplo anterior si se hubiese utilizado cutomers
en la asociación,
entonces surgiría un error indicando que Order::Customers
es desconocido.
Esto se debe a que rails infiere el nombre de la clase a partir del nombre de la asociación.
class CreateOrders < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.timestamps null: false
end
create_table :orders do |t|
t.belongs_to :customer, index: true
t.datetime :order_date
t.timestamps null: false
end
end
end
belongs_to
.Proveedores con una cuenta.
class CreateSuppliers < ActiveRecord::Migration
def change
create_table :suppliers do |t|
t.string :name
t.timestamps null: false
end
create_table :accounts do |t|
t.belongs_to :supplier, index: :unique
t.string :account_number
t.timestamps null: false
end
end
end
has_one
se coloca en la clase opuesta donde existe la clave foránea. Es similar a la asociaciónhas_many
pero en relaciones uno a uno en vez de uno a muchos.
belongs_to
.class CreateOrders < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.timestamps null: false
end
create_table :orders do |t|
t.belongs_to :customer, index: true
t.datetime :order_date
t.timestamps null: false
end
end
end
Es similar a la migración del ejemplo del
belongs_to
Turnos solicitados por pacientes para ser atendidos por médicos
Clases del modelo
class Physician < ActiveRecord::Base
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ActiveRecord::Base
belongs_to :physician
belongs_to :patient
end
class Patient < ActiveRecord::Base
has_many :appointments
has_many :physicians, through: :appointments
end
Migraciones
class CreateAppointments < ActiveRecord::Migration
def change
create_table :physicians do |t|
t.string :name
t.timestamps null: false
end
create_table :patients do |t|
t.string :name
t.timestamps null: false
end
create_table :appointments do |t|
t.belongs_to :physician, index: true
t.belongs_to :patient, index: true
t.datetime :appointment_date
t.timestamps null: false
end
end
end
Un proveedor tiene una cuenta y cada cuenta tiene asociado un histórico de cuenta
Clases del modelo
class Supplier < ActiveRecord::Base
has_one :account
has_one :account_history, through: :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ActiveRecord::Base
belongs_to :account
end
Migraciones
class CreateAccountHistories < ActiveRecord::Migration
def change
create_table :suppliers do |t|
t.string :name
t.timestamps null: false
end
create_table :accounts do |t:|
t.belongs_to :supplier, index: :unique
t.string :account_number
t.timestamps null: false
end
create_table :account_histories do |t|
t.belongs_to :account, index: :unique
t.integer :credit_rating
t.timestamps null: false
end
end
end
Crea una relación directa muchos a muchos con otro modelo sin un modelo interviniente.
Montaje de muchas piezas que puedan aparecer en muchos montajes
class CreateAssembliesAndParts < ActiveRecord::Migration
def change
create_table :assemblies do |t|
t.string :name
t.timestamps null: false
end
create_table :parts do |t|
t.string :part_number
t.timestamps null: false
end
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly, index: true
t.belongs_to :part, index: true
end
end
end
Un modelo puede pertenecer a uno o más modelos en una misma asociación
class Picture < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
class Employee < ActiveRecord::Base
has_many :pictures, as: :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, as: :imageable
end
Una imagen puede pertenecer a un empleado o un producto**
@employee.pictures
.@product.pictures
.@picture.imageable
. En este caso retornará instancias
de Product
o Employee
.
class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
t.timestamps null: false
end
add_index :pictures, :imageable_id
end
end
class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true,
index: true
t.timestamps null: false
end
end
end
Migración conveniente
class Employee < ActiveRecord::Base
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
belongs_to :manager, class_name: "Employee"
end
class CreateEmployees < ActiveRecord::Migration
def change
create_table :employees do |t|
t.references :manager, index: true
t.timestamps null: false
end
end
end
Al igual que con otros frameworks.
Conjunto de tecnologías o librerías utilizadas para desarrollar una aplicación.
Las componentes podrán intercambiarse fácilmente, habiendo múltiples alternativas. Seguir las tendencias o componentes populares es una buena elección
gem install rails
rails -v
--help
.rails new
para crear una aplicación Rails básicarails new --help
- La ayuda puede parecer extensa pero es importante leer las alternativas propuestas.
- No usaremos ninguna opción en esta primer instancia.
- Como resultado de la ejecución, se creará un directorio con carpetas y archivos en ella.
Asumimos ya instalada la gema de rails, corremos:
rails new ttps-ruby
El parámetro ttps-ruby indica el nombre del proyecto. Puede usarse cualquier nombre.
El instalador sin opciones ha instalado varias gemas más que vienen con una aplicación rails por defecto:
cd ttps-ruby && bundle list
La instalación por defecto requiere node y yarn para poder integrar webpacker.
Con los pasos anteriores hemos creado una aplicación simple con valores por defecto que ya puede usarse:
$ bundle exec rails server
Es posible que la aplicación no inicie si no se dispone de node/ yarn instalados.
Recordar la opción --help
para ver más opciones para el subcomando
server.
log/development.log
.Gemfile
.Archivo | Descripción |
---|---|
Gemfile[.loc] |
Gemas del proyecto y lock |
README.md |
Documentación en markdown |
app/ |
Carpetas y archivos de la aplicación |
config/ |
Carpetas y archivos de configuración |
db/ |
Carpetas y archivos de la DB |
public/ |
Archivos sin código ruby para ser servidos por un web server |
Rakefile |
Directivas para rake. Tareas de gestión del proyecto |
bin/ |
Ejecutables del proyecto |
config.ru |
Configuración para Rack |
lib/ |
Directorio para código ruby variado |
log/ |
Logfiles del proyecto |
tmp/ |
Archivos temporales del proyecto |
vendor/ |
Librerías externas por fuera del Gemfile |
Si listamos el contenido del directorio, nos encontramos con varias carpetas que estarán presentes en todo proyecto rails:
assets/
channels/
controllers/
helpers/
javascripts/
jobs/
mailers/
models/
views/
mailers/
contempla código para el envío de mails.helpers/
contiene view helpers, que son pequeñas porciones de
código reusable que generan HTML. Podríamos definirnos como macros que
expanden un pequeño comando en strings más extensos de tags HTML y contenido.assets/
contiene estilos CSS y Javascripts que son procesados
por assets pipeline, mientras que javascripts/
es utilizada por
webpacker.channels/
contiene código de websockets.jobs/
contiene tareas asíncronas.rails server
), comandos de consola y generadores.Estas gemas a su vez tienen dependencias, dando un total de aproximadamente 100 gemas.
Además de la gema de rails, el comando rails new
agrega otras gemas:
Gemas que podemos utilizar para implementar ciertas funcionalidades de forma más cómoda:
.env
en ENV
config.action_mailer.smtp_settings = {
address: "smtp.gmail.com",
port: 587,
domain: ENV["DOMAIN_NAME"],
authentication: "plain",
enable_starttls_auto: true,
user_name: ENV["GMAIL_USERNAME"],
password: ENV["GMAIL_PASSWORD"]
}
¿De qué forma seteamos los valores DOMAIN_NAME
, GMAIL_USERNAME
y
GMAIL_PASSWORD
?
Agregamos al Gemfile
gem "figaro", git: "https://github.com/laserlemon/figaro"
Instalamos la gema con bundler
bundle install
No han actualizado la gema en rubygems
config/application.yml
.config/application.yml
y
modificará .gitignore
para ignorar esta configuración.bundle exec figaro install
.Completamos la instalación con:
bundle exec figaro install
Editamos config/application.yml
GMAIL_USERNAME: mygmailusername
GMAIL_PASSWORD: mygmailpassword
development:
GMAIL_USERNAME: otherusername
GMAIL_PASSWORD: otherpassword
Probar con
rails console [-e production]
, imprimiendoENV['GMAIL_USERNAME']
config/credentials.yml.enc
.rails credentials:edit
:credentials.yml.enc
y versionarlo de
forma segura.rails credentials:edit
manejamos una configuración con todos
los parámetros de cada ambiente en forma jerárquica.rails credentials:edit --environment development
tendremos un
par de archivos por ambiente: config/credentials/development.yml.enc
config/credentials/development.key
Usamos desde rails las credenciales de la siguiente forma:
Rails.application.credentials[:some_key] # Only specific value
Rails.application.credentials.config # all configured values
Es importante diferenciar un application server de un web server.
bundle exec rails s
Creamos el archivo public/index.html
<h1> Hello World </h1>
Actualizamos la página y...rails observará los cambios de la carpeta
public/
. Si no se especifica ningún archivo en la URL, se asume index.html
¿Qué sucede si accedemos a /about.html?
Agregamos public/about.html
:
<h1> About </h1>
Si volvemos a probar, ahora todo debería funcionar bien.
public/
.public/index.html
Rails.application.routes.draw do
root to: redirect('/about.html')
end
Indicamos que al acceder a la raíz del proyecto redirija a
about.html
. El concepto de redirect utiliza mensajes HTTP 301
public/
.public/index.html
Notar cuando no es "fresco" el requerimiento
If-Modified-Since
.TZ=UTC stat public/about.html
.touch public/about.html
.Analizamos la consola del servidor:
Started GET "/" for 127.0.0.1 at ...
Es importante destacar que no hay logs para los archivos servidos desde la carpeta
public/
.
El siguiente gráfico muestra qué sucede en el servidor durante el ciclo request-response
Hay quienes opinan que la arquitectura de la web no se ajusta al original diseño de MVC creado para aplicaciones visuales de escritorio
config/routes.rb
y múltiples controladores, modelos y vistas.User
podría tener acciones para
listar los usuarios, agregar o eliminar un usuario de la lista.config/routes.rb
macheará el requerimiento web a una acción
del controlador.index
, show
, new
, create
, edit
, update
y
destroy
.Planificamos nuestro trabajo definiendo un User story que llamaremos Birthday countdown
Owner
y creamos el archivo app/models/owner.rb
.VisitorsController
considerando que visitor es el actor.app/controllers/visitors_controller.rb
.Modelos en singular usando camelcase:
class Visitor < ActiveRecord::Base
Controladores en plurar y camelcase:
class VisitorsController < ApplicationController
Terminan en
Controller
app/models/visitor.rb
.app/controllers/visitors_controller.rb
.app/views/visitors
.Siempre se utiliza snake case
Crearemos primero el ruteo antes de implementar el modelo y controlador. Editamos
entonces config/routes.rb
Rails.application.routes.draw do
root to: 'visitors#new'
end
La sintaxis de este archivo puede verse a partir del documento: Routing from outside in.
Modificando
config/routes.rb
no requiere reiniciar.
Acceder a http://localhost
Esperamos obtener un error indicando:
uninitialized constant VisitorsController
Lo cual es lógico considerando que no hemos implementado dicha clase.
rails generate model
para crear un modelo que hereda de ActiveRecord
.class Owner
def name
'Foobar Kadigan'
end
def birthdate
Date.new(1990, 12, 22)
end
def countdown
today = Date.today
birthday = Date.new(today.year,
birthdate.month,
birthdate.day)
if birthday > today
countdown = (birthday - today).to_i
else
countdown = (birthday.next_year - today).to_i
end
end
end
app/views/
.Creamos el directorio
mkdir app/views/visitors
Creamos app/views/visitors/new.html.erb
<h3>Home</h3>
<p>Welcome to the home of <%= @owner.name %>.</p>
<p>I was born on <%= @owner.birthdate %>.</p>
<p>Only <%= @owner.countdown %> days until my birthday!</p>
erb
porque usamos el motor ERB para armar nuestros
templates.<%=
y
%>
Por defecto en rails se utiliza ERB, pero es posible utilizar gemas que
proveen alternativas como por ejemplo Slim.
Si usáramos slim la vista sería new.html.slim
.
h3 Home in slim
p Welcome to the home of #{@owner.name}
p I was born on #{@owner.birthdate}.
p Only #{@owner.countdown} days until my birthday!
Debe incluirse la gema
slim-rails
y éste ser el único template dador que por defecto tiene prioridad erb.
El acceso a los datos del modelo Owner se hace a través de @owner
Podríamos preguntarnos por qué usar:
<%= @owner.countdown %>
en vez de
<%= (Date.new(today.year, @owner.birthdate.month, @owner.birthdate.day) - Date.today).to_i %>
Podríamos hacerlo, pero violaríamos SoC
VisitorsController#new
.VisitorsController
pero el nombre del archivo
visitors_controller.rb
Creamos app/controllers/visitors_controller.rb
class VisitorsController < ApplicationController
def new
@owner = Owner.new
end
end
¿Qué hace?
ApplicationController
hereda todo el comportamiento
definido por la API de rails.new
@owner
dado que en la vista
correspondiente estará disponible.app/views/visitors/new.html.erb
.new
es heredado de la API de rails.Indicando qué vista usar en el controlador
class VisitorsController < ApplicationController
def new
@owner = Owner.new
render 'visitors/new'
end
end
El concepto de scaffold lo utilizan varios frameworks para simplificar la generación de código a partir de templates parametrizables que aceleran la generación inicial de CRUD para modelos. Luego se podrá construir a partir de estos templates personalizando con código.
Rails, tiene un potente mecanismo de scaffolding que nos permitirá ir armando un proyecto rails de forma simple y automática.
La generación de:
rails generate scaffold books
El comando generará:
BooksController
Book
config/routes.rb
para resources :book
app/views/books
Lo probamos accediendo a:
y veremos que....
Primero debemos correr las migraciones
rails db:migrate
¡¡los libros no tienen campos!!
Para mejorar nuestro ejemplo, desharemos lo que hicimos y luego volveremos a correr el comando pero especificando algunos campos.
Eliminar lo generado por el scaffold
rails db:rollback
rails destroy scaffold books
Crear un nuevo scaffold con campos
rails generate scaffold books \
title:string author:string publication_year:integer
Rails provee generators para las diferentes componentes:
rails generate model Fruit name:string color:string
rails generate controller Fruit
rails generate resource post title:string body:text \
published:boolean
Es importante entender cómo los controladores funcionan en relación con las
rutas de rails. Podemos analizar entonces, luego de haber creado diferentes
rutas usando los generators, vemos que tenemos en config/routes.rb
Rails.application.routes.draw do
resources :posts
resources :books
root to: 'visitors#new'
end
Con el comando rails routes
podemos ver cómo se expanden esas rutas:
rails routes --help
rails routes -c posts
rails routes -E -c posts
En el ejemplo anterior, vemos que aparece resources :xxxx
. Esto define siete
acciones para implementar CRUD usando veebos HTTP. Si el recurso es photos
entonces tendremos:
Verbo HTTP | Path | Controller#Action | ¿Qué hace? |
---|---|---|---|
GET | /photos |
photos#index |
Listado de fotos |
GET | /photos/new |
photos#new |
Formulario para crear una nueva foto |
POST | /photos |
photos#create |
Crea la foto |
GET | /photos/:id |
photos#show |
Muestra una foto determinada |
GET | /photos/:id/edit |
photos#edit |
Formulario para editar una foto |
PATCH/PUT | /photos/:id |
photos#update |
Actualiza la foto |
DELETE | /photos/:id |
photos#destroy |
Elimina una foto |
resources :photos, :books, :videos
get 'profile', to: 'users#show'
resoruces :photo
genera seis acciones (no incluye el list)Person
que tiene asociado
Pet
. Es posible tener rutas como /people/:id/pets/new
Al crear una ruta usando resource, se generan además una serie de helpers que
pueden usarse en el controller y vistas, como por ejemplo para :photos
tenderemos entonces:
photos_path
: retornará /photosnew_photo_path
: retornará /photos/newedit_photo_path(:id)
: retornará /photos/:id/edit (entonces, edit_photo_path(10)
retornará /photos/10/edit)photo_path(:id)
: retornará /photos/:id (entonces, photo_path(10)
retornará /photos/10)Los mismos helpers con sufijo
_url
en vez de_path
proveen URLs absolutas
La mejor forma de aprender todas las capacidades del ruteo en rails, es recomendable leer Rails Routing from the Outside In.
Veremos algunas técnicas que ayudan a resolver problemas
bundle exec rails console
Notamos que se cargó el ambiente de development
log/development.log
log/production.log
rails server
.Para usar este objeto mostramos un ejemplo:
class VisitorsController < ApplicationController
def new
Rails.logger.debug 'DEBUG: entering new method'
@owner = Owner.new
Rails.logger.debug "DEBUG: Owner name is #{@owner.name}"
end
end
Rails.logger
en modelos.logger
directamentelogger.debug
logger.info
logger.warn
logger.error
logger.fatal
logger.debug
.Generamos un error
class VisitorsController < ApplicationController def new @owner = Owner.new DISASTER end end
Notar que el manejador en modo desarrollo deja una consola
¿Qué es?
I - Código fuente
II - Dependencias
III - Configuración
IV - Servicios
V - Construir, distribuir, ejecutar
VI - Procesos
VII - Asignación de puertos
VIII - Concurrencia
IX - Desechabilidad
X - Aproximación entre desarrollo y producción
XI - Logs
XII - Procesos de administración
Esta metodología se enuncia en https://12factor.net y describe un conjunto de prácticas para construir aplicaciones web o SaaS.
Hay relación entre entrega continua (construcción y distribución) y despliegue continuo (ejecución).
Dockerfile
.Cuando una aplicación debe atender procesos lentos, es aconsejable utilizar threads. Cuando esto no es posible, se aconseja utilizar procesos fuera de banda como es el caso de:
Las aplicaciones 12 factor deben estar preparadas para realizar despliegues continuo.
Hoy es muy común utilizar diferentes servicios como parte de un desarrollo. El armado del ambiente para desarrollo se simplifica utilizando herramientas como docker, docker-compose o vagrant.
Una aplicación 12 factor nunca se preocupa del direccionamiento, almacenamiento o rotación de logs.
En general debe escribir los logs en stdout y stderr.