Proceso secuencial
En este ejemplo no hay solapamiento
z, ? | toggle help (this) |
space, → | next slide |
shift-space, ← | previous slide |
d | toggle debug mode |
## <ret> | go to slide # |
c, t | table of contents (vi) |
f | toggle footer |
r | reload slides |
n | toggle notes |
p | run preshow |
P | toggle pause |
TTPS - Opcion
Ruby por Christian A. Rodriguez se encuentra bajo
una Licencia
Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.
Pero... ¿Cómo planificar sin saber qué hacer?
¿Si deseamos obtener validaciones frecuentes con el cliente?
¿Y con pruebas de regresión entre iteraciones?
Se requieren unos 40 minutos para realizar la actividad. Se trata de una sencilla e interesante actividad para ilustrar el concepto de Deuda Técnica. Pero más allá de dicho propósito, esta actividad permite motivar al inicio de un curso, lo diferente que es una estrategia de trabajo en equipo respecto de una estrategia de trabajo individual. Ver más en https://github.com/TTPS-ruby/capacitacion-ruby-ttps/blob/master/actividades/01-agiles/hard_choices.md
Para cada caso de uso implementado en la iteración, sus actividades se encadenan en una mini cascada
Estamos descubriendo formas mejores de desarrollar software tanto por nuestra propia experiencia como ayudando a terceros. A través de este trabajo hemos aprendido a valorar:
Individuos e interacciones sobre procesos y herramientas Software funcionando sobre documentación extensiva Colaboración con el cliente sobre negociación contractual Respuesta ante el cambio sobre seguir un plan
Esto es, aunque valoramos los elementos de la derecha, valoramos más los de la izquierda.
Nuestra mayor prioridad es satisfacer al cliente mediante la entrega temprana y continua de software con valor.
Aceptamos que los requisitos cambien, incluso en etapas tardías del desarrollo. Los procesos Ágiles aprovechan el cambio para proporcionar ventaja competitiva al cliente.
Entregamos software funcional frecuentemente, entre dos semanas y dos meses, con preferencia al periodo de tiempo más corto posible.
Los responsables de negocio y los desarrolladores trabajamos juntos de forma cotidiana durante todo el proyecto.
Los proyectos se desarrollan en torno a individuos motivados. Hay que darles el entorno y el apoyo que necesitan, y confiarles la ejecución del trabajo.
El método más eficiente y efectivo de comunicar información al equipo de desarrollo y entre sus miembros es la conversación cara a cara.
El software funcionando es la medida principal de progreso.
Los procesos Ágiles promueven el desarrollo sostenible. Los promotores, desarrolladores y usuarios debemos ser capaces de mantener un ritmo constante de forma indefinida.
La atención continua a la excelencia técnica y al buen diseño mejora la Agilidad.
La simplicidad, o el arte de maximizar la cantidad de trabajo no realizado, es esencial.
Las mejores arquitecturas, requisitos y diseños emergen de equipos auto-organizados.
A intervalos regulares el equipo reflexiona sobre cómo ser más efectivo para a continuación ajustar y perfeccionar su comportamiento en consecuencia.
SCRUM se basa en lo que se conoce como Sprints
Un sprint concentra el esfuerzo durante un período corto de tiempo hacia metas prefijadas
Las dos primeras preguntas ofrecen a los participantes una visión global del avance del proyecto. La tercera, sirve para solucionar problemas
$ git init
$ git clone https://github.com/rails/rails.git
Objetivo
git
Actividad
alumnos/
$ git checkout testing
$ vi test.rb
$ git commit -am 'made a change'
$ git checkout master
$ vi test.rb
$ git commit -am 'made other changes'
$ git checkout master
$ git merge hotfix
$ git checkout master
$ git merge iss53
$ git checkout experiment
$ git rebase master
cancionero/
Lecturas
Juegos:
Ruby is designed to make programmers HAPPY
NombreDeClaseOModulo
CONSTANTE
@nombre_de_atributo
@@atributo_de_clase
$variable_global
nombre_de_metodo
metodo_peligroso!
metodo_que_pregunta?
"Aprendiendo ruby".length
["Mateo", "Lola", "Lihue", "Clio"].sort
-100.abs
nil.nil?
1.object_id
nil.object_id
([1,2,3] + [4,5,6]).last
3
3.14
1_999_235_243_888 == 1999235243888
0b1000_1000 #Binario => 132
010 # Octal => 8
0x10 # Hexadecimal => 16
'sin interpolar'
"Interpolando: #{'Ja'*3}!"
%q/Hola/
%q!Chau!
%Q{Interpolando: #{3+3}}
un_string = <<EOS
Este es un texto
de mas de una linea
que termina aqui
EOS
un_string.upcase
:action
, :line_items
, :+
:uno.object_id # siempre devolverá lo mismo
"uno".object_id # siempre devolverá diferente
['Hola', 'Chau]
%w(Hola Chau #{2+2}) # sin interpolar
%W(Hola Chau #{2+2}) # interpolando
[1,2,3,4]
# Versión 1.8
{
:nombre => 'Christian',
:apellido => 'Rodriguez'
}
# Versión > 1.8
{
nombre: 'Christian',
apellido: 'Rodriguez'
}
0..1
0..10
"a".."z"
"a"..."z"
("a"..."z").to_a
(1..10) === 5 # => true
(1..10) === 15 # => false
(1..10) === 3.1 # => true
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
10 - 2
10.send :-, 2
[1,2,3] - [1]
a = 3.14
estado = nil
#...
face = case estado
when "Feliz" then ":)"
when "Triste" then ":("
else ":|"
end
def foo
"bar"
end
# => nil
Notar que la definición de un método retorna nil
Su ejecución retorna "bar"
3.times do |i|
puts i
end
# 0
# 1
# 2
# => 3 (retorna el 3 que recibe .times)
3.times { |x| puts x }
(1..10).select { |n| n.even? }
# o lo que es igual:
(1..10).select(& :even?)
(1..10).map { |n| n*2 }
# o lo que es igual:
(1..10).collect { |n| n*2 }
(1..100).reduce { |sum,n| sum + n }
# o lo que es igual:
(1..100).reduce(:+)
# La formula de verificacion es: n*(n-1)/2
100*101/2
File.open('my.txt').each do |line|
puts line if line =~ /ruby/
end
Si veo un pájaro que camina como pato, nada como pato y hace "cuack" como pato, entonces llamaré a ese pájaro un pato
public interface DuckLike {
Cuack cuack();
}
//...
public void doSomething(DuckLike d) {
d.cuack();
// ...
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DuckTyping {
interface Walkable { void walk(); }
interface Swimmable { void swim(); }
interface Quackable { void quack(); }
public static void main(String[] args) {
Duck d = new Duck();
Person p = new Person();
as(Walkable.class, d).walk(); //duck can walk()
as(Swimmable.class, d).swim(); //duck can swim()
as(Quackable.class, d).quack(); //duck can quack()
as(Walkable.class, p).walk(); //person can walk()
as(Swimmable.class, p).swim(); //person can swim()
// Gives Runtime Error
as(Quackable.class, p).quack(); //person can't quack()
}
//...
@SuppressWarnings("unchecked")
static <T> T as(Class<T> t, final Object obj) {
return (T) Proxy.newProxyInstance(t.getClassLoader(),
new Class[] {t},
new InvocationHandler() {
public Object invoke(Object proxy,
Method method,
Object[] args) throws Throwable {
try {
return obj.getClass()
.getMethod(method.getName(),
method.getParameterTypes())
.invoke(obj, args);
} catch (NoSuchMethodException nsme) {
throw new NoSuchMethodError(
nsme.getMessage());
} catch (InvocationTargetException ite) {
throw ite.getTargetException();
}
}
});
}
}
//....
class Duck {
public void walk() {
System.out.println("I'm Duck, I can walk...");
}
public void swim() {
System.out.println("I'm Duck, I can swim...");
}
public void quack() {
System.out.println("I'm Duck, I can quack...");
}
}
class Person {
public void walk() {
System.out.println("I'm Person, I can walk...");
}
public void swim() {
System.out.println("I'm Person, I can swim...");
}
public void talk() {
System.out.println("I'm Person, I can talk...");
}
}
class Duck
def quack
puts "Quaaaaaack!"
end
def feathers
puts "The duck has white and gray feathers."
end
end
class Person
def quack
puts "The person imitates a duck."
end
def feathers
puts "The person takes a feather from the ground"
end
end
def in_the_forest duck
duck.quack
duck.feathers
end
donald = Duck.new
john = Person.new
in_the_forest donald
in_the_forest john
(1..10).even # da error: even no existe
class Range
# Agregamos even a Range
def even
self.select(& :even?)
end
end
(1..10).even # ahora no da error
# => [2,4,6,8,10]
module MyAPI
class User
...
end
def self.configuration
...
end
end
user = MyAPI::User.new
puts MyAPI::configuration
Como las interfaces, pero con comportamiento
module MyLog
def log(msg)
puts "Log: #{msg}"
end
end
class String; include MyLog; end
"hola".log("pepe")
#Log: pepe
# => nil
cd
para cambiar de versión de ruby~/.rbenv
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
~/.rbenv/bin
a $PATH
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
En ubuntu, hacer el echo
en .profile
en vez de .bash_profile
rbenv init
al shell para habilitar los shims y el autocompletado
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l
~/.rbenv/plugins
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
~/.rbenv/plugins
$ git clone https://github.com/sstephenson/rbenv-gem-rehash.git ~/.rbenv/plugins/rbenv-gem-rehash
~/.rbenv/plugins
$ git clone https://github.com/rkh/rbenv-update.git ~/.rbenv/plugins/rbenv-update
rbenv versions
muestra las versiones instaladas de ruby (con un * la versión actual)rbenv global
muestra o setea la versión global de ruby
$ rbenv global # muestra la versión global
$ rbenv global 2.0.0-p247 # setea la versión 2.0.0-p247 como global
rbenv local
identico al comando anterior, pero para el directorio actual
rbenv install
instala rubies! (con -l
listamos todas las versiones disponibles)
Luego de haber recibido una dosis de Ruby ya conocemos un poco sobre la sintáxis de este lenguaje
Para entender mejor los objetos, vamos a hacerlo mediante un ejemplo -tomado del libro Programming Ruby (Pick Axe):
Reventa de libros usuados que realiza control de stock semanalmente
Mediante lectores de códigos de barra se registra cada libro en las bibliotecas. Cada lector, genera un archivo separado por comas (CSV) que contiene una fila para cada libro registrado.
Cada fila contiene entre otros datos: ISBN del libro y precio. Un extracto del archivo sería:
"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
BookInStock
Recordamos que los nombres de las clases deben comenzar con mayúsculas, los métodos con minúscula
class BookInStock
end
Lo probamos:
a_book = BookInStock.new
another_book = BookInStock.new
En el ejemplo anterior:
BookInStock
. Lo solucionamos obligando que la inicialización indique aquellos datos que distinga al libro
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 objetosinitialize
initialize
@isbn
e isbn
están relacionadas, pero:
Float
toma un argumento y lo convierte a float
, terminando el programa si falla
la conversiónAnalizar cómo es que Float
es un método
b1 = BookInStock.new("isbn1", 3)
p b1
b2 = BookInStock.new("isbn2", 3.14)
p b2
b3 = BookInStock.new("isbn3", "5.67")
p b3
p
porque imprime el estado interno de los objetos. puts
por defecto imprime #<nombre_de_clase:id_objeto_en_hex>
to_s
, que es enviado a cualquier objeto que necesita convertirse a string
to_s
class BookInStock
def to_s
"ISBN: #{@isbn}, price: #{@price}"
end
end
BookInStock
con el fin de agregar atributos
para isbn
y price
así podemos contabilizarlosclass BookInStock
def isbn
@isbn
end
def price
@price
end
end
accesor
porque mapean
directamente con las variables de instanciaattr_reader
attr_reader
class BookInStock
attr_reader :isbn, :price
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
# ..
end
attr_reader
define variables de instancia, y no lo hace
price=
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)
puts "ISBN = #{book.isbn}"
puts "Price = #{book.price}"
book.price = book.price * 0.75 # discount price
puts "New price = #{book.price}"
attr_reader
ruby provee un shortcut para accessors de
sólo escritura: attr_writer
(raramente usado)attr_accessor
que provee acceso R/Wclass BookInStock
attr_reader :isbn
attr_accessor :price
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
end
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
price
e isbn
price
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")
: : :
puts "Total value in stock = \
#{reader.total_value_in_stock}"
CsvReader
debe ir acumulando lo que va leyendo de cada csvrequire '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
read_in_csv_data
la primer línea indica la apertura del archivo
csv_file_name
y el parámetro headers: true
indica a la librería que la
primer línea del archivo son los encabezados de cada columna o campoclass CsvReader
# Luego veremos como usar inject...
def total_value_in_stock
sum = 0.0
@books_in_stock.each do |book|
sum += book.price
end
sum
end
end
book_in_stock.rb
: contendrá la clase BookInStock
csv_reader.rb
: será el código de CsvReader
stock_stats.rb
: será el programa principalrequire
y require_relative
stock_stats.rb
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}"
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
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.
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}"
person1 = "Tim"
person2 = person1
person1.freeze
person2[0] = 'J'
array
hash
o arreglo asociativoArray
mantiene una colección de referencias a objetos. 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"
b
[]
[]
[]
es un método (de instancia en la clase Array
) y por tanto puede
implementarse por cualquier subclasenil
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]
Acceder arrays con dos valores indica [start,count]
y retorna siempre un
nuevo array
a = [ 1, 3, 5, 7, 9]
a[1..3]
a[1...3]
a[3..3]
a[-3..-1]
..
se incluye el fin de rango...
se excluye el extremo final[]=
[]=
permite setear elementos de un array[]=
se completa con nila = [ 1, 3, 5, 7, 9 ]
a[1] = 'bat'
a[-3] = 'cat'
a[3] = [ 9, 8 ]
a[6] = 99
[]=
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
Array
push
pop
stack = []
stack.push "red"
stack.push "green"
stack.push "blue"
p stack
puts stack.pop
puts stack.pop
puts stack.pop
p stack
Array
unshift
shift
queue = []
queue.push "red"
queue.push "green"
puts queue.shift
puts queue.shift
array = [ 1, 2, 3, 4, 5, 6, 7 ]
p array.first(4)
p array.last(4)
h = { 'dog' => 'canine', 'cat' => 'feline' }
h.length # => 3
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
def words_from_string(string)
string.downcase.scan(/[\w']+/)
end
Asumimos que counts es un Hash
if counts.has_key?(next_word)
counts[next_word] += 1
else
counts[next_word] = 1
end
Hash.new
puede recibir como parámetro el valor usado para incializar cada
valor del Hash. (por ejemplo cuando se accede a un valor que no existe aún en
el Hash)def count_frequency(word_list)
counts = Hash.new(0)
for word in word_list
counts[word] += 1
end
counts
end
Ver el ejemplo completo en la carpeta samples/05/words_frequency
Test::Unit
que ya incluye ruby como
parte de la librería estándarassert_equal
que chequea si los dos parámetros
que se le envían son iguales, indicando fuertemente si así no
sucedewords_from_string
require_relative 'words_from_string.rb'
require 'test/unit'
class TestWordsFromString < Test::Unit::TestCase
def test_empty_string
assert_equal([], words_from_string(""))
assert_equal([], words_from_string(" "))
end
def test_single_word
assert_equal(["cat"], words_from_string("cat"))
assert_equal(["cat"], words_from_string(" cat "))
end
def test_many_words
assert_equal(["the", "cat", "sat", "on","the","mat"],
words_from_string("the cat sat on the mat"))
end
def test_ignores_punctuation
assert_equal(["the", "cat's", "mat"],
words_from_string("<the!> cat's, -mat...-"))
end
end
En esta clase, los métodos que comienzan con test serán corridos por el framework de testing
count_frequency
require_relative 'count_frequency.rb'
require 'test/unit'
class TestCountFrequency < Test::Unit::TestCase
def test_empty_list
assert_equal({}, count_frequency([]))
end
def test_single_word
assert_equal({"cat" => 1}, count_frequency(["cat"]))
end
def test_two_different_words
assert_equal({"cat" => 1, "sat" => 1},
count_frequency(["cat", "sat"]))
end
def test_two_words_with_adjacent_repeat
assert_equal({"cat" => 2, "sat" => 1},
count_frequency(["cat", "cat", "sat"]))
end
def test_two_words_with_non_adjacent_repeat
assert_equal({"cat" => 2, "sat" => 1},
count_frequency(["cat", "sat", "cat"]))
end
end
samples/05/words_frequency/top_five
usamos:for i in 0...5
word = top_five[i][0]
count = top_five[i][1]
puts "#{word}: #{count}"
end
En cualquier lenguaje esto es natural
top_five.each do |word, count|
puts "#{word}: #{count}"
end
# o más simple:
top_five.each { |word, count| "#{word}: #{count}" }
do
y end
do
/ end
|
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 bloque
sum
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!
value = "some shape"
[ 1, 2 ].each {|value| puts value }
puts value
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" }
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 verdaderoeach
y collect
each
es el más simple
yield
para cada elementocollect
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 }
["H", "A", "L"].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
inject
[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}
inject
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 iterador
a = [1,2,3].each
a.next
loop
loop
terminará cuando el Enumerator se quede sin valoresloop { 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}" }
Enumerator
como objetoseach_with_index
en Array
result = []
[ 'a', 'b', 'c' ].each_with_index do |item, index|
result << [item, index]
end
Enumerator
como objetosString
?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
# Incluso Matz nos simplifico mas...
result = []
"cat".each_char.with_index do |item, index|
result << [item, index]
end
Enumerator
como generadoresyield
yield
Enumerator
como generadoresfibonacci = 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 }
Enumerator
es Enumerable
fibonacci.first(1000).last
count
y select
tratarán de
leer todos los elementos antes de retornar un valor
select
adecuada a nuestra lista
infinitodef 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)
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 argsFile.open(*args)
. Utilizar *args vuelve a expandir los
elementos del arreglo a parámetros individualesFile.open
ya lo implementa
File.open
para abrir un archivo, podemos usarlo para
directamente procesarlo como lo hacíamos con open_and_process
my_open
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
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)
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"
Proc
y lambda
lamda
y Proc.new
toman un bloque y retornan un objetoProc
lambda
y Proc.new
la veremos más adelante, pero ya
hemos mencionado que lambda
controla los parámetros que requiere el
bloque, mientras que Proc
no lo hacedef 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)
n_times
referencia el parámetro thing
que es usado por el bloquecall
(y por ende en la ejecución del bloque) el
parámetro thing
está fuera del alcance, el parámetro se mantiene accesible
dentro del bloquedef 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
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"
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" }
class Parent
def say_hello
puts "Hello from #{self}"
end
end
p = Parent.new
p.say_hello
# Subclass the parent...
class Child < Parent
end
c = Child.new
c.say_hello
superclass
devuelve la clase padreputs "The superclass of Child is #{Child.superclass}"
puts "The superclass of Parent is #{Parent.superclass}"
puts "The superclass of Object is #{Object.superclass}"
Object
to_s
está definido aquíBasicObject
es utilizado en metaprogramación.
nil
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)
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
LogServer
hereda de GServer
initialize
super
super
, Ruby envía el método a la clase padre del objeto
actual, indicando que invoque el mismo método que se está ejecutando en el
hijo. Se enviarán los parámetros que fueron pasados a super
serve
es algo común en OO
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 Trig.sin(x)
# ..
end
def Trig.cos(x)
# ..
end
end
module Moral
VERY_BAD = 0
BAD = 1
def Moral.sin(badness)
# ...
end
end
y = Trig.sin(Trig::PI/4)
wrongdoing = Moral.sin(Moral::VERY_BAD)
Trig.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
include
en Ruby agrega una referencia al módulo que agregará nuevos
métodos a nuestra claseComparable
<
, <=
, ==
, >=
, >
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?
Enumerable
each
<=>
entonces
dispondremos de:
min
max
sort
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(:+)
[ 1, 2, 3, 4, 5 ].inject(:+)
( 'a'..'m').inject(:+)
Summable
module Summable
def sum
inject(:+)
end
end
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
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
'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
US-ASCII
en
1.9 y UTF-8
a partir de ruby 2#encoding: xxxx
donde xxx corresponde a la codificación#encoding: iso-8859-1
txt = "dog"
puts "Encoding of #{txt.inspect} is #{txt.encoding}"
min
, max
, include
, etc100.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 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, luego de los parámetros normales logramos este efecto
def varargs(arg1, *rest)
"arg1=#{arg1}. rest=#{rest.inspect}"
end
super
super
, entonces se invoca el método del
padre con todos los argumentos que se hayan recibidoclass 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 arreglodef 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))
(1..10).collect { |x| x*2}.join(',')
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
})
{}
, 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)
log
def log(msg, level: "ERROR", time: Time.now)
puts "#{ time.ctime } [#{ level }] #{ msg }"
end
def log(msg, opt = {})
level = opt[:level] || "ERROR"
time = opt[:time] || Time.now
puts "#{ time.ctime } [#{ level }] #{ msg }"
end
log("Hello!", level: "INFO")
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
log("Hello")
log("Hello!", level: "ERROR", time: Time.now)
log("Hello!", time: Time.now, level: "ERROR")
log(level: "ERROR", time: Time.now, "Hello!")
log("Hello!", date: Time.new)
**
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)
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") { }
a, b, c = 1, 2, 3
a * b + c
# O en forma similar
(a.*(b)).+(c)
class Fixnum
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'
o
%x` para indicar la ejecución de un comando en el
sistema operativo subyacentedate`
ls`.split[34]
%x{echo "Hello there"}
`ip add ls`.
split("\n").
select {|x| x =~ / inet / }.
map do |x|
x.scan(/((\d{1,3}\.?){4}\/(\d){1,2})/).flatten.shift
end
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
&&
y el método and
funcionan similar
and
es de menor precedencia que &&
nil && 99 # => nil
false && 99 # => false
"cat" && 99 # => 99
|| y el método
or` funcionan similar
or
es de menor precedencia que ||
nil || 99 # => 99
false || 99 # => 99
"cat" || 99 # => "cat"
||=
para setear un valor si no fue
seteado: var ||= "default value"
break
: termina en forma inmediata al loop en que 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ónException
se propagará hacia arriba en la pila de ejecución hasta
que el sistema detecte código que sepa manejar dicha excepciónException
Exception
Exception
o con una clase propia que sea subclase de StandardError
o alguna
de sus hijas. require 'open-uri'
web_page = open("http://pragprog.com/podcasts")
output = File.open("podcasts.html", "w")
while line = web_page.gets
output.puts line
end
output.close
require 'open-uri'
page = "podcasts"
file_name = "#{page}.html"
web_page = open("http://pragprog.com/#{page}")
output = File.open(file_name, "w")
begin
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
Exception
StandardError
ArgumentError
FiberError (1.9)
IndexError
KeyError (1.9)
StopIteration (1.9)
IOError
EOFError
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SystemCallError
system-dependent exceptions (Errno::xxx)
ThreadError
TypeError
ZeroDivisionError
Exception
Exception
fatal
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SecurityError
SignalException
Interrupt
SystemExit
SystemStackError
rescue
rescue
para un bloque begin
rescue
puede incluso indicar varias excepciones a catchearrescue
, podemos indicar el nombre de la variable que
usaremos para mapear la exepción (en vez de usar $!
)begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom
rescue StandardError => bang
print "Error running script: " + bang
end
rescue
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 pilarescue
, pero
podemos usar expresiones que retornen una subclase de Exception
ensure
cumple esta funciónensure
se ejecutará siempre, haya sido una ejecución exitosa
o con algún problemaensure
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close
end
else
de rescue
else
aplica cuando ninguno de los rescue
manejan la excepciónensure
ejecutará siempre, incluso cuando no se produce
un errorf = 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
retry
@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
Kernel.raise
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
RuntimeError
si no.
Generalmente se utiliza dentro de un manejador de excepciónRuntimeError
con el mensaje indicadoKernel.caller
normalmente se utiliza para generar la traza de
ejecución)raise
raise
raise "Missing name" if name.nil?
if i >= names.size
raise IndexError, "#{i} >= size (#{names.size})"
end
raise ArgumentError, "Name too big", caller
catch
y throw
word_list = File.open("wordlist")
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
Juan desarrolla una funcionalidad que tiene algún error no detectado. Unos dos meses después, desarrollamos determinada funcionalidad que, indirectamente utiliza lo que Juan ha desarrollado.
Cuando nuestro código no devuelve los resultados esperados, nos llevará un tiempo encontrar el problema dentro del código de Juan. Es entonces cuando consultamos con Juan:
¿Por qué encaraste la solución así?
y la respuesta inmediata será:
no recuerdo, fue hace varios meses
consiste en correr un programa que invocan una parte del código de nuestra aplicación, obtiene algunos resultados y verifica que dichos resultados sean los esperados
Roman
que permita crear objetos con un valor
numérico y que imprima el valor como un número romanoclass Roman
MAX_ROMAN = 4999
def initialize(value)
if value <= 0 || value > MAX_ROMAN
fail "Roman values must be > 0 and <= #{MAX_ROMAN}"
end
@value = value
end
FACTORS = [["m",1000], ["cm",900], ["d",500], ["cd",400],
["c",100], ["xc",90], ["l",50], ["xl",40],
["x",10], ["ix",9], ["v",5], ["iv",4], ["i",1]]
def to_s
value = @value
roman = ""
for code, factor in FACTORS
count, value = value.divmod(factor)
roman << code unless count.zero?
end
roman
end
end
irb
require 'roman'
r = Roman.new(1)
fail "'i' expected" unless r.to_s == "i"
r = Roman.new(9)
fail "'ix' expected" unless r.to_s == "ix"
Test::Unit
era la opción de la mayor parte de los
desarrolladoresassertions
de MiniTest
se espejan con las
definidas en Test::Unit::TesCase
assert_not_raises
y assert_not_throws
en MiniTest
Test::Unit
se usa assert_not_nil(x)
y assert_not(x), en
MiniTest usaremos
refute_nil(x)
y refute(x)
require 'minitest/unit'
y usaremos únicamente la funcionalidad de MiniTestrequire 'test/unit'
y usaremos MiniTest con la capa de compatibilidad con
Test::Unit habilitada, agregando las assertions ausentes y la funcionalidad de
autorun mencionadastest-unit
y disponer de la funcionalidad original de
Test::Unitif/unless
se utilizan assertions que
provee el framework de testrequire 'roman'
require 'test/unit'
class TestRoman < MiniTest::Unit::TestCase
def test_simple
assert_equal("i", Roman.new(1).to_s)
assert_equal("ix", Roman.new(9).to_s)
end
end
Lo probamos
require 'roman'
require 'test/unit'
class TestRoman < MiniTest::Unit::TestCase
def test_simple
assert_equal("i", Roman.new(1).to_s)
assert_equal("ii", Roman.new(2).to_s)
assert_equal("iii", Roman.new(3).to_s)
assert_equal("iv", Roman.new(4).to_s)
assert_equal("ix", Roman.new(9).to_s)
end
end
Lo probamos
to_s
def to_s
value = @value
roman = ""
for code, factor in FACTORS
count, value = value.divmod(factor)
roman << (code * code)
# cambiamos: unless count.zero?
end
roman
end
Lo probamos
require 'roman'
require 'test/unit'
class TestRoman < Test::Unit::TestCase
NUMBERS = [
[ 1, "i" ], [ 2, "ii" ], [ 3, "iii" ],
[ 4, "iv"], [ 5, "v" ], [ 9, "ix" ]
]
def test_simple
NUMBERS.each do |arabic, roman|
r = Roman.new(arabic)
assert_equal(roman, r.to_s)
end
end
end
Roman
require 'roman'
require 'test/unit'
class TestRoman < Test::Unit::TestCase
def test_range
# no exception for these two...
Roman.new(1)
Roman.new(4999)
# but an exception for these
assert_raises(RuntimeError) { Roman.new(0) }
assert_raises(RuntimeError) { Roman.new(5000) }
end
end
Roman
que genera números hasta 4999. Ver carpeta samples/10/romans
assert | refute(boolean, [ message ] )
# Fails if boolean is (is not) false or nil.
assert_block { block }
# Expects the block to return true.
assert_ | refute_empty(collection, [ message ] )
# Expects empty? on collection to return true (false).
assert_ | refute_equal(expected, actual, [ message ] )
# Expects actual to equal/not equal expected, using ==.
assert_ | refute_in_delta(expected_float, actual_float,
delta, [ message ] )
# Expects that the actual floating-point value is (is not)
# within delta of the expected value.
assert_ | refute_in_epsilon(expected_float, actual_float,
epsilon=0.001, [ message ] )
# Calculates a delta value as epsilon * min(expected, actual),
# then calls the _in_delta test.
assert_ | refute_includes(collection, obj, [ message ] )
# Expects include?(obj) on collection to return true (false).
assert_ | refute_instance_of(klass, obj, [ message ] )
# Expects obj to be (not to be) a instance of klass.
assert_ | refute_kind_of(klass, obj, [ message ] )
# Expects obj to be (not to be) a kind of klass.
assert_ | refute_match(regexp, string, [ message ] )
# Expects string to (not) match regexp.
assert_ | refute_nil(obj, [ message ] )
# Expects obj to be (not) nil.
assert_ | refute_operator(obj1, operator, obj2, [ message ] )
# Expects the result of sending the message operator to obj1
# with parameter obj2 to be (not to be) true.
assert_raises(Exception, . . . ) { block }
# Expects the block to raise one of the listed exceptions.
assert_ | refute_respond_to(obj, message, [ message] )
# Expects obj to respond to (not respond to) message (a symbol).
assert_ | refute_same(expected, actual, [ message ] )
# Expects expected.equal?(actual).
assert_send(send_array, [ message ] )
# Sends the message in send_array[1] to the receiver in
# send_array[0], passing the rest of send_array as arguments.
# Expects the return value to be true.
assert_throws(expected_symbol, [ message ] ) { block }
# Expects the block to throw the given symbol.
flunk(message="Epic Fail!")
# Always fails.
skip(message)
# Indicates that a test is deliberately not run.
pass
# Always passes.
refute_nil
que devolvería Expected nil to not be nilLos tests de unidad nos llevan a dos agrupamientos:
Roman
alcanza con un único
test case. Las clases que representan test cases, deben ser subclase de:
Test::Unit::TestCase
Generalmente queremos ejecutar determinado código antes y luego de cada test. Disponemos entonces de:
setup
teardown
samples/10/setup-teardown
gem install rake
gem search sinatra
gem list
gem install bundler
Gemfile
source 'https://rubygems.org'
gem 'sinatra'
bundle install
o simplemente bundle
bundle install
bundle update
bundle exec
bundle list
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'
Gemfile
no basta, hay que invocar a bundler.require 'bundler'
Bundler.require
require 'bundler'
Bundler.setup
require 'sinatra'
Un mensaje HTTP se compone por:
require 'sinatra'
get '/' do
'hello world'
end
server.rb
y luego lo ejecutamos (ruby server.rb
)$ curl -v http://localhost:4567/
rack-test
GET /
y esperaremos que nos
devuelva un código de estado 200
(OK) y que el body sea 'hello world'
require 'minitest/autorun'
require 'rack/test'
require_relative 'server'
class HelloWorldTest < Minitest::Test
include Rack::Test::Methods
def app
Sinatra::Application
end
def test_get_root
get '/'
assert_equal 200, last_response.status
assert_equal 'hello world', last_response.body
end
end
require 'sinatra'
get '/hello/:name' do
# Todo lo que venga en el query string
# será capturado en params, por ejemplo:
# '/hello/Patricio?username=patricio'
"Hello #{params[:name]}"
end
public
views
get '/' do
@name = 'Frank Sinatra'
erb :index
end
<html>
<head>
<title>Welcome</title>
</head>
<body>
Hello, <%= @name %>!
</body>
</html>
before
son evaluados antes de cada petición
dentro del mismo contexto que las rutas. Pueden modificar la petición y la
respuesta. Las variables de instancia asignadas en los filtros son accesibles
por las rutas y las plantillas (idem con after
):before do
@nota = 'Hey!'
end
get '/' do
@nota #=> 'Hey!'
end
helpers do
def bar(name)
"#{name}bar"
end
end
get '/:name' do
"#{bar params[:name]}"
end
enable :sessions
get '/' do
"value = " << session[:value].inspect
end
get '/:value' do
session[:value] = params[:value]
end
get '/foo' do
redirect to('/bar')
end
get '/bar' do
'Hello!'
end
not_found do
'Ruta no encontrada'
end
error do
'Disculpá, ocurrió un error horrible - ' +
env['sinatra.error'].name
end
class Product < ActiveRecord::Base
end
La clase Product
se mapea automáticamente a la tabla llamada products
, que
podría ser algo como:
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
Además se definen los siguientes accessors: Product#name
y
Product#name=(new_name)
Ver ejemplo 13/00-intro-ar
y notar que no funcionará si no existe la tabla
creada
Book
se mapea a books
BookClub
se mapeará a la tabla
book_clubs
+---------------+---------------+
| Model / Class | Table / Schema|
+---------------+---------------+
| Post | posts |
| LineItem | line_items |
| Deer | deers |
| Mouse | mice |
| Person | people |
+---------------+---------------+
nombre_en_singular_id
(por ejemplo: item_id
, order_id
). Estos serán
los campos por los que Active Record buscará cuando se creen asociaciones entre
modelosid
como clave primaria. Cuando se usan Migraciones de
Active Record para crear las tablas, esta columna se creará automáticamentecreated_at
: esta columna automáticamente setea la fecha y hora cuando el
registro es creadoupdated_at
: esta columna automáticamente setea la fecha y hora cuando el
registro es actualizadolock_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)_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.ActiveRecord::Base
y listoclass Product < ActiveRecord::Base
end
p = Product.new
p.name = "Some Book"
puts p.name # "Some Book"
Product
y la tabla products
. new
retornará un objeto nuevo mientras que create
retornará
un objeto y lo guardará en la base de datosuser = User.create(name: "David",
occupation: "Code Artist")
# es lo mismo que:
user = User.new
user.name = "David"
user.occupation = "Code Artist"
user.save
user = User.new do |u|
u.name = "David"
u.occupation = "Code Artist"
end
funciona tanto con new
como create
# 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')
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'"
user = User.find_by(name: 'David')
user.destroy
create
, save
y update
consideran las
validaciones. false
cuando la validación falla y no actualizan el dato en la
base de datoscreate!
,
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
# => false
User.create!
# => ActiveRecord::RecordInvalid:
# Validation failed:
# Name can't be blank
rake
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
Un stack es un conjunto de tecnologías o librerías utilizadas para desarrollar una aplicación o para servir una página
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
Successfully installed rails-4.0.1
$ rails -v
Rails 4.0.1
rails new
rails new
para crear una aplicación Rails básicarails new
:$ rails new --help
Primero creamos nuestra aplicación
$ rails new vearn-rails
El parámetro learn-rails indica el nombre del proyecto. Puede usarse cualquier nombre
Se instalarán varias gemas nuevas usando bundler
$ cd learn-rails
Con los pasos anteriores hemos creado una aplicación simple con valores por defecto que ya puede usarse
Es posible iniciar la aplicación usando rails server
o rails s
$ bundle exec rails s
... Could not find a JavaScript runtime....
Para solucionar este error debe instalarse nodejs o agregar la gema
therubyracer al Gemfile
(notar que ya está pero comentada)
$ bundle exec rails s
=> Booting WEBrick
=> Rails 4.0.1 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2013-11-30 19:41:53] INFO WEBrick 1.3.1
[2013-11-30 19:41:53] INFO ruby 2.0.0 (2013-06-27) [x86_64-linux]
[2013-11-30 19:41:53] INFO WEBrick::HTTPServer#start: pid=15338 port=3000
log/development.log
Gemfile
Gemfile | Lists all the gems used by the application. |
Gemfile.lock | Lists gem versions and dependencies. |
README.rdoc | A page for documentation. |
app/ | Application folders and files. |
config/ | Configuration folders and files. |
db/ | Database folders and files. |
public/ | Files for web pages that do not contain Ruby code, such as error pages. |
No son importantes cuando estamos aprendiendo rails...
Rakefile | Directives for the Rake utility program. |
bin/ | Folder for binary (executable) programs. |
config.ru | Configuration file for Rack (a software library for web servers). |
lib/ | Folder for miscellaneous Ruby code. |
log/ | Folder for application server logfiles. |
tmp/ | Temporary files created when your application is running. |
vendor/ | Folder for Ruby software libraries that are not gems. |
app/
mailers/
contempla código para el envío de mailshelpers/
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 contenidoassets/
contiene estilos CSS y Javascripts que son procesados
por sprocketsEstas gemas a su vez tienen dependencias, dando un total de aproximadamente 44 gemas
rails new
agrega otras gemas:
Puede que no se utilice ni SQLite, SCSS, jQuery u otras gemas, pero la mayoría de los desarrollos las utilizan y por ello se consideran
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?
gem 'figaro'
$ bundle install
...
Using rails (4.0.1)
Installing figaro (0.7.0)
Using jbuilder (1.5.2)
...
$ bundle exec rails generate
...
Figaro:
figaro:install
...
config/application.yml
rails generate figaro:install
config/application.yml
y apendea al
.gitignore
que se ignore esta configuracion
$ bundle exec rails generate figaro:install
create config/application.yml
append .gitignore
config/application.yml
GMAIL_USERNAME: mygmailusername
GMAIL_PASSWORD: mygmailpassword
development:
GMAIL_USERNAME: otherusername
GMAIL_PASSWORD: otherpassword
Podemos setear las variables según el entorno
bundle exec rails s
public/index.html
<h1> Hello World </h1>
public/
public/about.html
<h1> About </h1>
Ahora todo debería funcionar bien
public/
por defecto
rm public/index.html
config/routes.rb
LearnRails::Application.routes.draw do
root to: redirect('/about.html')
end
public/
public/index.html
Cliqueando sobre el nombre del archivo, y luego sobre la solapa Headers se visualiza el detalle del requerimiento y su respuesta
Ahora podemos analizar cómo el requerimiento a http://localhost:3000/ devuelve dos entradas por el redirect
Started GET "/" for 127.0.0.1 at ...
public/
El siguiente gráfico muestra qué sucede en el servidor durante el ciclo request-response
Algunos expertos 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 vistasconfig/routes.rb
macheará el requermiento web a una acción del
controladorindex
, show
, new
, create
, edit
, update
y
destroy
Planificamos nuestro trabajo definiendo un User story
*Birthday countdown*
As a visitor to the website
I want to see the owner's name
I want to see the owner's birthdate
I want to see how many days until the
owner's next birthday
In order to send birthday greetings
app/models/owner.rb
app/controllers/visitors_controller.rb
class Visitor < ActiveRecord::Base
- los nombres de las clases de modelo
son en singular y en mayúsculaclass VisitorsController < ApplicationController
- los nombres de
controladores son la combinación de un nombre de modelo en singular con
Controller en camel caseapp/models/visitor.rb
app/controllers/visitors_controller.rb
app/views/visitors
Crearemos primero el ruteo antes de implementar el model y controller
config/routes.rb
LearnRails::Application.routes.draw do
root to: 'visitors#new'
end
rails
new learn_rails
config/routes.rb
pueden
entenderse bien leyendo Rails Guide: Routing from outside inconfig/routes.rb
no requiere reiniciar la aplicación
El error es claro: uninitialized constant VisitorsController indicando que Rails busca la clase y no puede encontrarla
Podemos mejorar el error agregando la gema better_errors
al Gemfile
Si hacemos caso al mensaje marcado en rojo, podemos agregar nua consola REPL
como irb que nos permitirá debugear el código.
El mensaje sugiere agregar la gema binding_of_caller
al Gemfile
rails
generate model
para crear un modelo que hereda de ActiveRecord
y conoce como conectarse con la base de datosclass Owner
def name
name = 'Foobar Kadigan'
end
def birthdate
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/
mkdir app/views/visitors
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>
<%=
y
%>
@owner
<%= @owner.countdown %>
en vez de <%=
(Date.new(today.year, @owner.birthdate.month, @owner.birthdate.day) -
Date.today).to_i %>
VisitorsController
pero el nombre del archivo
visitors_controller.rbapp/controllers/visitors_controller.rb
class VisitorsController < ApplicationController
def new
@owner = Owner.new
end
end
new
@owner
dado que en la vista
correspondiente estará disponible. app/views/visitors/new.html.erb
class VisitorsController < ApplicationController
def new
@owner = Owner.new
render 'visitors/new'
end
end
rails generate scaffold
que permite crear MVC en una única operación
$ bundle exec rails console
Loading development environment (Rails 4.0.1)
irb(main):001:0>
Notamos que se cargó el ambiente de development
irb(main):001:0> owner = Owner.new
=> #<Owner:0x007f7eccd77e48>
irb(main):002:0> owner.name
=> "Foobar Kadigan"
rails server
app/controllers/visitors_controller.rb
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
Started GET "/" for 127.0.0.1 at ...
Processing by VisitorsController#new as HTML
DEBUG: entering new method
DEBUG: Owner name is Foobar Kadigan
Rendered visitors/new.html.erb within layouts/application (0.2ms)
Completed 200 OK in 8ms (Views: 4.6ms | ActiveRecord: 0.0ms)
logger.debug
logger.info
logger.warn
logger.error
logger.fatal
logger.debug
class VisitorsController < ApplicationController
def new
Rails.logger.debug 'DEBUG: entering new method'
@owner = Owner.new
Rails.logger.debug 'DEBUG: Owner name is ' + @owner.name
DISASTER
end
end
Started GET "/" for 127.0.0.1 at 2013-12-08 20:09:18 -0300
Processing by VisitorsController#new as HTML
DEBUG: entering new method
DEBUG: Owner name is Foobar Kadigan
Completed 500 Internal Server Error in 2ms
NameError - uninitialized constant VisitorsController::DISASTER:
activesupport (4.0.1) lib/active_support/dependencies.rb:501:in `load_missing_constant'
...
class VisitorsController < ApplicationController
def new
Rails.logger.debug 'DEBUG: entering new method'
@owner = Owner.new
Rails.logger.debug ".."
raise 'Deliberate Failure'
end
end