├── .gitignore ├── explicaciones ├── 04 │ ├── public │ │ └── favicon.ico │ ├── .ruby-version │ ├── Gemfile │ ├── Gemfile.lock │ ├── server.rb │ ├── day_checker.rb │ ├── smiley.rb │ └── README.md ├── 03 │ ├── testing │ │ ├── .simplecov │ │ ├── lib │ │ │ └── calculadora.rb │ │ ├── Rakefile │ │ ├── test │ │ │ └── calculadora_test.rb │ │ └── spec │ │ │ └── calculadora_spec.rb │ └── exceptions │ │ └── motivacion └── 02 │ ├── guest.rb │ ├── building.rb │ ├── person.rb │ ├── client.rb │ ├── README.md │ ├── office.rb │ ├── acme.rb │ ├── employee.rb │ ├── acme │ └── storage.rb │ ├── storable.rb │ └── mixins.rb ├── README.md ├── vagrant ├── README.md └── Vagrantfile ├── practicas ├── 04-gems-rack-sinatra.md ├── 05-rails.md ├── 03-excepciones-testing.md ├── 01-intro-git-ruby.md └── 02-clases-modulos-bloques-enumerators.md └── evaluaciones └── 2019 └── tpi └── enunciado.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vagrant/.vagrant 2 | -------------------------------------------------------------------------------- /explicaciones/04/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /explicaciones/04/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.5 2 | -------------------------------------------------------------------------------- /explicaciones/03/testing/.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start 2 | -------------------------------------------------------------------------------- /explicaciones/04/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra', '~> 2.0' 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Este repositorio está desactualizado y fue sucedido por [el nuevo repositorio de práctica](https://github.com/TTPS-ruby/practicas).** 2 | -------------------------------------------------------------------------------- /explicaciones/03/testing/lib/calculadora.rb: -------------------------------------------------------------------------------- 1 | class Calculadora 2 | def sumar(a, b) 3 | a + b 4 | end 5 | 6 | def restar(a, b) 7 | a - b 8 | end 9 | 10 | def dividir(a, b) 11 | a / b 12 | end 13 | 14 | def multiplicar(a, b) 15 | a * b 16 | end 17 | 18 | def raiz_cuadrada(a) 19 | Math.sqrt(a) 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /explicaciones/02/guest.rb: -------------------------------------------------------------------------------- 1 | require_relative 'person' 2 | require_relative 'employee' 3 | 4 | class Guest < Person 5 | attr_accessor :grants, :inviter_id 6 | 7 | # Este método permite obtener el objeto `Employee` identificado por el valor que tenemos almacenado en el atributo 8 | # `inviter_id`. 9 | def inviter 10 | Employee.find!(inviter_id) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /explicaciones/02/building.rb: -------------------------------------------------------------------------------- 1 | require_relative 'storable' 2 | 3 | class Building 4 | include Storable 5 | 6 | attr_accessor :id, :name, :address 7 | 8 | # Definimos un constructor básico para poder establecer los atributos de la nueva instancia en el mismo `new`. 9 | def initialize(id, name, address) 10 | self.id = id 11 | self.name = name 12 | self.address = address 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /explicaciones/02/person.rb: -------------------------------------------------------------------------------- 1 | require_relative 'storable' 2 | 3 | class Person 4 | include Storable 5 | 6 | attr_accessor :id, :name, :phone_number 7 | 8 | # Definimos un constructor básico para poder establecer los atributos de la nueva instancia en el mismo `new`. 9 | def initialize(id, name, phone_number = nil) 10 | self.id = id 11 | self.name = name 12 | self.phone_number = phone_number 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /explicaciones/04/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | mustermann (1.0.3) 5 | rack (2.2.3) 6 | rack-protection (2.0.7) 7 | rack 8 | sinatra (2.0.7) 9 | mustermann (~> 1.0) 10 | rack (~> 2.0) 11 | rack-protection (= 2.0.7) 12 | tilt (~> 2.0) 13 | tilt (2.0.10) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | sinatra (~> 2.0) 20 | 21 | BUNDLED WITH 22 | 1.16.2 23 | -------------------------------------------------------------------------------- /explicaciones/02/client.rb: -------------------------------------------------------------------------------- 1 | require_relative 'person' 2 | require_relative 'employee' 3 | 4 | class Client < Person 5 | attr_accessor :representative_id 6 | 7 | # @see Employee.type_for_storage 8 | def self.type_for_storage 9 | superclass.name 10 | end 11 | 12 | # Este método permite obtener el objeto `Employee` identificado por el valor que tenemos almacenado en el atributo 13 | # `representative_id`. 14 | def assigned_representative 15 | Employee.find(representative_id) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /explicaciones/03/testing/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.libs << "lib" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | end 8 | 9 | Rake::TestTask.new(:spec) do |t| 10 | t.libs << "spec" 11 | t.libs << "lib" 12 | t.test_files = FileList["spec/**/*_spec.rb"] 13 | end 14 | 15 | task :coverage do |t| 16 | `open coverage/index.html` 17 | end 18 | 19 | task :clean do |t| 20 | FileUtils.rm_rf 'coverage' 21 | end 22 | 23 | task default: :test 24 | -------------------------------------------------------------------------------- /explicaciones/02/README.md: -------------------------------------------------------------------------------- 1 | # TTPS opción Ruby 2 | 3 | # Explicación de Práctica 2 4 | 5 | En esta explicación veremos el uso de módulos como _Mixins_. Para probar y seguir la explicación, podés abrir el archivo 6 | `mixins.rb` de este mismo directorio y a partir de éste ver la implementación y documentación que hemos armado para 7 | documentar esta técnica de implementación de herencia horizontal en Ruby. 8 | 9 | ## Cómo probarlo 10 | 11 | Con una shell ubicada en el mismo directorio que está este `README`, ejecutá el script `mixins.rb`: 12 | 13 | ```console 14 | $ ruby mixins.rb 15 | ``` 16 | -------------------------------------------------------------------------------- /explicaciones/04/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require_relative 'day_checker' 3 | require_relative 'smiley' 4 | 5 | set :public_folder, 'public' 6 | 7 | # Utilizar middleware Smiley 8 | use Smiley 9 | 10 | # GET / 11 | # Chequea si el día actual es el día por defecto chequeado por `DayChecker` 12 | get '/' do 13 | DayChecker.is?(Date.today) 14 | end 15 | 16 | # GET /lunes o GET /1 17 | # Chequea si el día actual es el día recibido como parámetro 18 | get '/:day' do 19 | DayChecker.is? Date.today, params[:day] 20 | end 21 | 22 | puts "Server iniciado. Ahora podés acceder a http://localhost:#{settings.port}/ y probar" 23 | -------------------------------------------------------------------------------- /explicaciones/04/day_checker.rb: -------------------------------------------------------------------------------- 1 | class DayChecker 2 | DAYS = { 3 | 'domingo' => 0, 4 | 'lunes' => 1, 5 | 'martes' => 2, 6 | 'miercoles' => 3, 7 | 'jueves' => 4, 8 | 'viernes' => 5, 9 | 'sabado' => 6 10 | }.freeze 11 | 12 | def self.is?(date, expected_day = nil) 13 | expected_day = sanitize(expected_day || 5) 14 | date.wday == expected_day ? 'Si' : 'No' 15 | end 16 | 17 | def self.sanitize(day) 18 | if day.to_s =~ /^\d+$/ 19 | day.to_i % 7 20 | elsif DAYS.keys.include?(day) 21 | DAYS[day] 22 | else 23 | raise "Invalid day provided: '#{day}'." 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /explicaciones/04/smiley.rb: -------------------------------------------------------------------------------- 1 | class Smiley 2 | def initialize(app) 3 | @app = app 4 | end 5 | 6 | def call(env) 7 | status, headers, response = @app.call(env) 8 | new_response = '' 9 | response.each { |l| new_response << smileify(l) } 10 | headers['Content-Length'] = new_response.length.to_s 11 | [status, headers, [new_response]] 12 | end 13 | 14 | def smileify(string) 15 | if string =~ /\b(Si|No)\b/ 16 | "#{string} #{smiley_for(string)}" 17 | else 18 | string 19 | end 20 | end 21 | 22 | def smiley_for(string) 23 | case string 24 | when /Si/ 25 | ':)' 26 | when /No/ 27 | ':(' 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /explicaciones/02/office.rb: -------------------------------------------------------------------------------- 1 | require_relative 'storable' 2 | require_relative 'building' 3 | 4 | class Office 5 | include Storable 6 | 7 | attr_accessor :id, :number, :floor, :building_id 8 | 9 | # Definimos un constructor básico para poder establecer los atributos de la nueva instancia en el mismo `new`. 10 | def initialize(id, number, floor, building) 11 | self.id = id 12 | self.number = number 13 | self.floor = floor 14 | self.building_id = building.id 15 | end 16 | 17 | # Este método permite obtener el objeto `Building` identificado por el valor que tenemos almacenado en el atributo 18 | # `building_id`. 19 | def building 20 | Building.find(building_id) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /explicaciones/02/acme.rb: -------------------------------------------------------------------------------- 1 | # Este módulo funciona como un _namespace_ para las clases que nos son provistas por la compañía ACME. 2 | module ACME 3 | # Esta sentencia nos ahorra tener que incluir tanto este archivo (`acme.rb`) como también cualquier archivo donde se 4 | # definan clases dentro de este módulo. 5 | # El funcionamiento de `autoload` es el siguiente: nosotros declaramos en este módulo que existe una constante llamada 6 | # `:Storage` e indicamos dónde está definida. De esa forma, la primera vez que se la referencie, se irá a cargar la 7 | # definición que existe en el archivo que indicamos antes, evitando tener que requerir explícitamente esos archivos 8 | # en cada lugar que vayamos a utilizar las clases (o cualquier elemento) de este módulo. 9 | autoload :Storage, './acme/storage.rb' 10 | end 11 | -------------------------------------------------------------------------------- /explicaciones/03/exceptions/motivacion: -------------------------------------------------------------------------------- 1 | Queremos implementar un REPL simple para evaluar código Ruby. 2 | 3 | Un REPL funciona como un bucle en que: 4 | 5 | 1. se lee la entrada del usuario, 6 | 2. se evalúa esa entrada, 7 | 3. se imprime el resultado de la evaluación, y 8 | 4. se vuelve al paso 1. 9 | 10 | En caso que ocurran errores, el REPL debería informarlos y seguir funcionando (al menos en la mayoría de los casos). 11 | 12 | Para simplificar su implementación, queda fuera del alcance de nuestro REPL la evaluación de sentencias multilínea. 13 | 14 | Nuestro REPL debe guardar un registro (pensar en un `history` de la terminal) de los comandos ejecutados, persistente entre ejecuciones del REPL. Ese registro debe poder consultarse escribiendo `history` o simplemente `h`, y se debe brindar una ayuda de los comandos disponibles si se igresa `help`. 15 | 16 | -------------------------------------------------------------------------------- /explicaciones/03/testing/test/calculadora_test.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'minitest/autorun' 3 | require 'calculadora' 4 | 5 | class CalculadoraTest < Minitest::Test 6 | def setup 7 | @calculadora = Calculadora.new 8 | end 9 | 10 | def test_suma_ceros 11 | assert_equal 0, @calculadora.sumar(0, 0) 12 | end 13 | 14 | def test_suma_positivos 15 | assert_equal 1, @calculadora.sumar(1, 0) 16 | assert_equal 2, @calculadora.sumar(1, 1) 17 | end 18 | 19 | def test_suma_positivos_negativos 20 | assert_equal 1, @calculadora.sumar(2, -1) 21 | end 22 | 23 | def test_suma_negativos 24 | assert_equal (-2), @calculadora.sumar(-1, -1) 25 | assert_equal (-1), @calculadora.sumar(-1, 0) 26 | end 27 | 28 | def test_suma_no_importa_orden 29 | assert_equal @calculadora.sumar(10, 2), @calculadora.sumar(2, 10) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /explicaciones/03/testing/spec/calculadora_spec.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'minitest/autorun' 3 | require 'calculadora' 4 | 5 | describe Calculadora do 6 | before do 7 | @calculadora = Calculadora.new 8 | end 9 | 10 | describe 'Suma' do 11 | it 'correctamente ceros' do 12 | _(@calculadora.sumar(0, 0)).must_equal 0 13 | end 14 | 15 | it 'correctamente valores positivos' do 16 | _(@calculadora.sumar(1, 0)).must_equal 1 17 | _(@calculadora.sumar(1, 1)).must_equal 2 18 | end 19 | 20 | it 'correctamente valores positivos y negativos' do 21 | _(@calculadora.sumar(2, -1)).must_equal 1 22 | end 23 | 24 | it 'correctamente negativos' do 25 | _(@calculadora.sumar(-1, -1)).must_equal(-2) 26 | _(@calculadora.sumar(-1, 0)).must_equal(-1) 27 | end 28 | 29 | it 'es indiferente al orden de los sumandos' do 30 | _(@calculadora.sumar(10, 2)).must_equal @calculadora.sumar(2, 10) 31 | end 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /explicaciones/02/employee.rb: -------------------------------------------------------------------------------- 1 | require_relative 'person' 2 | require_relative 'office' 3 | 4 | # Notá que esta y las demás clases que heredan de `Person` no necesitan incluir el módulo `Storable` porque ya se lo 5 | # incluyó en la superclase. 6 | class Employee < Person 7 | attr_accessor :file_number, :office_id 8 | 9 | # Definimos un constructor básico para poder establecer los atributos de la nueva instancia en el mismo `new`. 10 | def initialize(id, name, phone_number = nil, file_number = nil, office = nil) 11 | super(id, name, phone_number) 12 | self.file_number = file_number 13 | self.office_id = office.id unless office.nil? 14 | end 15 | 16 | # Como queremos que las instancias de la clase `Employee` se guarden junto con las de `Person`, sobreescribimos la 17 | # implementación genérica que Storable.type_for_storage provee con una específica a nuestro caso: devolvemos el nombre 18 | # de la superclase. 19 | def self.type_for_storage 20 | superclass.name 21 | end 22 | 23 | # Este método permite obtener el objeto `Office` identificado por el valor que tenemos almacenado en el atributo 24 | # `office_id`. 25 | def office 26 | Office.find(office_id) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /vagrant/README.md: -------------------------------------------------------------------------------- 1 | # VM con Ruby 2.6.3 2 | 3 | Este directorio contiene un archivo de configuración de [Vagrant](https://www.vagrantup.com) que genera una máquina 4 | virtual con Ruby 2.6.3 instalado para facilitar la realización de los ejercicios de las prácticas. 5 | 6 | > Nota: la versión de la imagen base utilizada de Ubuntu requiere utilizar Vagrant 2.1 o superior para funcionar. 7 | 8 | ## Prerequisitos 9 | 10 | Se deben tener instalados [Vagrant](https://vagrantup.com) y [Virtualbox](https://virtualbox.org) instalados en la 11 | máquina donde se va a crear la VM. Virtualbox es el software utilizado para correr las máquinas virtuales, mientras 12 | que Vagrant es el encargado de preparar las VMs según el manifesto de configuración `Vagrantfile` (incluido en el 13 | mismo directorio que este archivo `README`). 14 | 15 | ## Uso 16 | 17 | Para crear la máquina virtual o iniciarla, basta con situarse en este directorio y ejecutar el siguiente comando: 18 | 19 | ```console 20 | $ vagrant up 21 | ``` 22 | 23 | Una vez hecho esto, para poder usar la VM deben ingresar a la misma ejecutando el comando 24 | 25 | ```console 26 | $ vagrant ssh 27 | ``` 28 | 29 | Una vez finalizado el uso de la VM, se la puede apagar para que no ocupe recursos mediante el comando 30 | 31 | ```console 32 | $ vagrant halt 33 | ``` 34 | 35 | (Recordar que la próxima vez se deberá ejecutar nuevamente `vagrant up` antes de hacer `vagrant ssh`) 36 | -------------------------------------------------------------------------------- /explicaciones/04/README.md: -------------------------------------------------------------------------------- 1 | # TTPS opción Ruby 2 | 3 | # Explicación de Práctica 4 4 | 5 | Este proyecto demuestra un caso sencillo de uso de las siguientes herramientas: 6 | 7 | * Bundler 8 | * Rack 9 | * Sinatra 10 | 11 | La aplicación web basada en Sinatra es una implementación muy sencilla del típico "¿Ya es viernes?". La misma provee los 12 | siguientes puntos de consulta: 13 | 14 | * `GET /` 15 | Contesta sí o no, en función de si la fecha actual es viernes. 16 | * `GET /:day` 17 | Contesta sí o no, acorde a si la fecha actual es `:day`, donde éste parámetro puede ser: 18 | * Un número (`0..6`) indicando el índice del día de semana con el cual comparar, basado en `0` y comenzando a contar 19 | desde el domingo. 20 | * Un string (`domingo`, `lunes`, `martes`, `miercoles`, `jueves`, `viernes`, `sabado`) indicando el nombre en español 21 | del día de la semana con el cual comparar. 22 | 23 | Esta aplicación utiliza Bundler para manejar sus dependencias, Sinatra para definir el comportamiento del server web, e 24 | implementa un *middleware* Rack que agrega *smileys* a la respuesta en función de su contenido, a modo de ejemplo de las 25 | posibilidades que brindan estos elementos. 26 | 27 | ## Cómo probarlo 28 | 29 | ```console 30 | $ bundle 31 | $ bundle exec ruby server.rb 32 | ``` 33 | 34 | Y luego acceder en el navegador a la URL que sugiere la salida del comando. 35 | 36 | Algunas URLs para probar: 37 | 38 | * `/` 39 | * `/1` 40 | * `/lunes` 41 | * `/miercoles` 42 | * `/viernes` 43 | * `/osvaldo` 44 | * `/7` 45 | 46 | > `curl http://localhost:4567/lunes` 47 | -------------------------------------------------------------------------------- /vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | DESIRED_VERSION = '2.6.3' 5 | 6 | Vagrant.configure(2) do |config| 7 | # More boxes at https://app.vagrantup.com/boxes/search 8 | config.vm.box = "ubuntu/bionic64" 9 | 10 | config.vm.network "private_network", ip: "10.10.10.10" 11 | 12 | config.vm.provider "virtualbox" do |vb| 13 | vb.name = "TTPS-Ruby" 14 | vb.gui = false 15 | vb.memory = "1024" 16 | end 17 | 18 | # This creates a shared (synced) folder from the host in the guest. Customize it at will, or add new ones. 19 | config.vm.synced_folder '.', '/home/vagrant/host' 20 | 21 | # Provision the box with a shell script. This installs rbenv + ruby-build and Ruby (+ dependencies). 22 | user = 'vagrant' 23 | home = "/home/#{user}" 24 | rbenv_path = "#{home}/.rbenv" 25 | profile_path = "#{home}/.bashrc" 26 | ruby_version = DESIRED_VERSION 27 | rbenv_abs_path = "#{rbenv_path}/bin/rbenv" 28 | gem_abs_path = "#{rbenv_path}/shims/gem" 29 | config.vm.provision "shell", inline: <<-SHELL 30 | sudo apt-get update -qq 31 | sudo DEBIAN_FRONTEND=noninteractive apt-get install -y git autoconf bison build-essential lib{ssl,yaml,sqlite3}-dev libreadline{7,-dev} zlib1g{,-dev} 32 | if [ ! -d #{rbenv_path} ]; then 33 | git clone https://github.com/sstephenson/rbenv.git #{rbenv_path} 34 | cd #{rbenv_path} && src/configure && make -C src 35 | git clone https://github.com/sstephenson/ruby-build.git #{rbenv_path}/plugins/ruby-build 36 | echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> #{profile_path} 37 | echo 'eval "$(rbenv init -)"' >> #{profile_path} 38 | else 39 | cd #{rbenv_path} && git pull origin master 40 | cd #{rbenv_path}/plugins/ruby-build && git pull origin master 41 | fi 42 | chown -R #{user}:#{user} #{rbenv_path} 43 | sudo -i -H -u #{user} /bin/bash -lc 'RUBY_CONFIGURE_OPTS=--disable-install-doc #{rbenv_abs_path} install -s #{ruby_version} && #{rbenv_abs_path} global #{ruby_version} && #{gem_abs_path} update --system' 44 | SHELL 45 | end 46 | -------------------------------------------------------------------------------- /explicaciones/02/acme/storage.rb: -------------------------------------------------------------------------------- 1 | # Esta clase implementa un almacenamiento en memoria de valores identificados por una clave, agrupados por un tipo que 2 | # se obtiene enviando el método `#type` al valor cuando se lo guarda. 3 | # 4 | # Por ejemplo, para guardar el objeto `my_object` puede utilizarse el siguiente código: 5 | # 6 | # class Country 7 | # attr_accessor :iso_code, :name 8 | # 9 | # def initialize(iso_code, name) 10 | # self.iso_code = iso_code 11 | # self.name = name 12 | # end 13 | # 14 | # def type 15 | # :country 16 | # end 17 | # end 18 | # 19 | # argentina = Country.new(:ar, 'Argentina') 20 | # ACME::Storage[argentina.iso_code] = argentina 21 | # 22 | # Y luego, puede volver a obtenerse ese valor del almacenamiento de la siguiente manera: 23 | # 24 | # ACME::Storage[:country, :ar].name 25 | # # => "Argentina" 26 | # 27 | # Pueden obtenerse todos los identificadores almacenados para un tipo particular: 28 | # 29 | # ACME::Storage[:country].keys 30 | # # => [:ar] 31 | # 32 | # O pueden obtenerse todos los valores almacenados para un tipo particular: 33 | # 34 | # ACME::Storage[:country].values 35 | # # => [#] 36 | module ACME 37 | class Storage 38 | # Método que permite almacenar un valor en el storage. 39 | def self.[]=(key, value) 40 | store[value.type][key] = value 41 | rescue NoMethodError 42 | raise "Unable to store object of class #{value.class} as it doesn't respond to #type" 43 | end 44 | 45 | # Método que permit obtener o bien todos los valores almacenados para un tipo en este storage, o bien un valor 46 | # particular identificado por `key`. 47 | def self.[](type, key = nil) 48 | scope = store[type] 49 | scope = scope[key] unless key.nil? 50 | scope 51 | end 52 | 53 | # Este método permite obtener todos los tipos de objetos con valores en este storage. 54 | def self.types 55 | store.keys 56 | end 57 | 58 | def self.store 59 | @store ||= Hash.new do |hash, key| 60 | hash[key] = {} 61 | end 62 | end 63 | private_class_method :store 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /explicaciones/02/storable.rb: -------------------------------------------------------------------------------- 1 | require_relative 'acme' 2 | 3 | # Este módulo es la implementación de nuestro Mixin: nos permite encapsular la lógica que queremos compartir en las 4 | # distintas clases que deben poder persistirse en el `ACME::Storage` sin necesidad de crear una jerarquía de clases que 5 | # en realidad no lo es por el simple hecho de que las clases compartan cierta parte de su comportamiento. 6 | module Storable 7 | # Este módulo contiene los métodos que terminarán siendo de clase en aquellas que incluyan `Storable`. 8 | # @see Storable.included 9 | module ClassMethods 10 | # Método de clase para recuperar un elemento del storage por su clave. 11 | def find(key) 12 | storage[type_for_storage, key] 13 | end 14 | 15 | # Alternativa al método .find de este módulo que arroja una excepción si no encuentra un valor para la clave `key`. 16 | # @see .find 17 | def find!(key) 18 | find(key) || raise("No element found for key #{key} of type #{type_for_storage}") 19 | end 20 | 21 | # Implementación básica del método #type que `ACME::Storage` requiere que los objetos a guardar en éste respondan. 22 | # Por defecto, utilizamos el nombre de la clase como tipo. 23 | def type_for_storage 24 | name 25 | end 26 | 27 | # Definir un método para encapsular el backend de storage que vamos a usar nos facilitaría cambiarlo si así lo 28 | # quisiéramos, con solo modificar o reimplementar este método. 29 | def storage 30 | ACME::Storage 31 | end 32 | end 33 | 34 | # Delegamos el método #type que requiere `ACME::Storage` en el método de clase .type_for_storage. 35 | def type 36 | self.class.type_for_storage 37 | end 38 | 39 | # Método que permite almacenar un objeto en el storage. 40 | def store 41 | self.class.storage[store_id] = self 42 | end 43 | 44 | # Implementación básica del método #store_id utilizado internamente por este módulo para almacenar los objetos en el 45 | # `ACME::Storage`. Si el objeto responde a `#id`, se utiliza ese valor como clave, caso contrario se utiliza el 46 | # `object_id` del objeto. 47 | def store_id 48 | if respond_to? :id 49 | id 50 | else 51 | object_id 52 | end 53 | end 54 | 55 | # Este método se invocará cuando este módulo sea incluido, recibiendo como parámetro `base` la clase (o el módulo) que 56 | # está incluyendo nuestro módulo. 57 | def self.included(base) 58 | # Acá se muestra el uso del módulo interno `ClassMethods`: encapsula en un _submódulo_ los métodos que serán de 59 | # clase, de manera tal que al incluir nuestro módulo seamos nosotros los encargados de extender `base` y agregar 60 | # como métodos de clase aquellos contenidos por el submódulo `ClassMethods`. Esto libera al usuario de nuestro 61 | # módulo de tener que hacer el `extend` explícitamente y beneficia el encapsulamiento de la lógica, al no ser 62 | # necesario que nuestro usuario conozca la implementación interna del módulo para utilizarlo, sólo debe conocer la 63 | # interfaz pública (API externa) del mismo. 64 | base.extend(ClassMethods) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /explicaciones/02/mixins.rb: -------------------------------------------------------------------------------- 1 | # Ejemplo de uso de módulos como Mixins para el Taller de Producción de Software - Opción Ruby. 2 | # 3 | # Este script muestra _una_ forma de utilizar módulos en Ruby como interfaces con comportamiento o, lo que es similar, 4 | # como un mecanismo de herencia horizontal entre clases de diferentes jerarquías (también conocida como "herencia 5 | # múltiple"). 6 | # 7 | # En este ejemplo demostraremos, además, un patrón común para definir métodos de instancia y de clase en un mismo Mixin 8 | # mediante el uso del módulo `ClassMethods` (el nombre no es obligatorio). 9 | # 10 | # Motivación 11 | # ========== 12 | # 13 | # Se nos pide implementar un módulo que permita almacenar en memoria instancias de distintas clases, permitiendo 14 | # utilizar así un mecanismo muy sencillo de caching de objetos. Ya disponemos de una clase `ACME::Storage` para utilizar 15 | # que mantiene un diccionario de claves y valores, agrupados por el _tipo_ de cada valor que se obtiene mediante el 16 | # envío del mensaje `type` al objeto que se intenta guardar. 17 | # 18 | # Las clases que utilizarán este mecanismo de persistencia son 6: 19 | # * `Person` 20 | # * `Employee` (subclase de `Person`) 21 | # * `Client` (subclase de `Person`) 22 | # * `Guest` (subclase de `Person`) 23 | # * `Building` 24 | # * `Office` 25 | # 26 | # Cada una de estas clases posee sus propios atributos y lógica que exceden el alcance de este ejemplo, por lo que vamos 27 | # a obviarlos, simplemente agregando algunos atributos como para que las pruebas tengan sentido. 28 | # 29 | # Referencias 30 | # =========== 31 | # 32 | # * Modules, "The Pragmatic Programmer's Guide", David Thomas & Andrew Hunt. 33 | # http://ruby-doc.com/docs/ProgrammingRuby/html/tut_modules.html 34 | 35 | # Estos archivos que incluimos son la implementación del ejemplo. En este archivo únicamente haremos uso de lo que se 36 | # define en ellos, a modo de ensayo y prueba de cómo podrían utilizarse esas clases y módulos. 37 | require_relative 'acme' 38 | require_relative 'storable' 39 | require_relative 'person' 40 | require_relative 'employee' 41 | require_relative 'client' 42 | require_relative 'guest' 43 | require_relative 'building' 44 | require_relative 'office' 45 | 46 | tato = tota = rectorado = oficina = empleado = nil 47 | 48 | def find_and_compare(v) 49 | v == v.class.find(v.id) ? '✓' : '' 50 | end 51 | 52 | def section(&b) 53 | puts "" 54 | yield 55 | print '> ' 56 | $stdin.gets 57 | end 58 | 59 | section do 60 | puts "Al iniciar, el storage está vacío. Tiene #{ACME::Storage.types.count} tipos almacenados." 61 | end 62 | 63 | section do 64 | puts "Entonces creamos y almacenamos algunos objetos..." 65 | tato = Person.new(1, 'Tato').store 66 | puts " - Tato" 67 | tota = Person.new(2, 'Tota').store 68 | puts " - Tota" 69 | rectorado = Building.new(1, 'Rectorado UNLP', 'Avenida 7 nro 776').store 70 | puts " - Rectorado" 71 | oficina = Office.new(10, 101, 1, rectorado).store 72 | puts " - Oficina 101" 73 | empleado = Employee.new(3, 'Empleado del mes', nil, '1238/2', oficina).store 74 | puts " - Empleado del mes" 75 | end 76 | 77 | section do 78 | puts "Luego, el storage tiene valores de los siguientes tipos: #{ACME::Storage.types.join(', ')}." 79 | end 80 | 81 | section do 82 | puts "Podemos chequear si lo que ACME::Storage tiene en su store interno coincide con lo que guardamos en él:" 83 | puts " - Tato: #{find_and_compare(tato)}" 84 | puts " - Tota: #{find_and_compare(tota)}" 85 | puts " - Rectorado: #{find_and_compare(rectorado)}" 86 | puts " - Oficina: #{find_and_compare(oficina)}" 87 | puts " - Empleado: #{find_and_compare(empleado)}" 88 | end 89 | 90 | section do 91 | begin 92 | no_existe = Person.find!(9) 93 | puts "Si llegamos hasta acá, alguien guardó una persona con id = 9." 94 | rescue RuntimeError => e 95 | puts "No hay una persona con id = 9, por eso obtuvimos una excepción que dice \"#{e.message}\"." 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /practicas/04-gems-rack-sinatra.md: -------------------------------------------------------------------------------- 1 | # TTPS opción Ruby 2 | 3 | # Práctica 4 4 | 5 | En esta práctica veremos ejercicios para comprender el funcionamiento de las gemas en Ruby, el uso de Bundler y 6 | comenzaremos a utilizar la herramienta para desarrollo de aplicaciones web en Ruby [Sinatra](http://sinatrarb.com/). 7 | 8 | ## Gemas y Bundler 9 | 10 | 1. ¿Qué es una gema? ¿Para qué sirve? ¿Qué estructura tienen? 11 | 2. ¿Cuáles son las principales diferencias entre el comando `gem` y Bundler? ¿Hacen lo mismo? 12 | 3. ¿Dónde instalan las gemas los comandos `gem` y `bundle`? 13 | > Tip: `gem which` y `bundle show`. 14 | 4. ¿Para qué sirve el comando `gem server`? ¿Qué información podés obtener al usarlo? 15 | 5. Investigá un poco sobre *Semantic Versioning* (o *SemVer*). ¿Qué finalidad tiene? ¿Cómo se compone una versión? ¿Ante 16 | qué situaciones debería cambiarse cada una de sus partes? 17 | 6. Creá un proyecto para probar el uso de Bundler: 18 | 1. Inicializá un proyecto nuevo en un directorio vacío con el comando `bundle init` 19 | 2. Modificá el archivo `Gemfile` del proyecto y agregá la gema `colorputs` 20 | 3. Creá el archivo `prueba.rb` y agregale el siguiente contenido: 21 | 22 | ```ruby 23 | require 'colorputs' 24 | puts "Hola!", :rainbow_bl 25 | ``` 26 | 27 | 4. Ejecutá el archivo anterior de las siguientes maneras: 28 | - `ruby prueba.rb` 29 | - `bundle exec ruby prueba.rb` 30 | 5. Ahora utilizá el comando `bundle install` para instalar las dependencias del proyecto 31 | 6. Volvé a ejecutar el archivo de las dos maneras enunciadas en el paso **4**. 32 | 7. Creá un nuevo archivo `prueba_dos.rb` con el siguiente contenido: 33 | ```ruby 34 | Bundler.require 35 | puts "Chau!", :red 36 | ``` 37 | 8. Ahora ejecutá este nuevo archivo: 38 | - `ruby prueba_dos.rb` 39 | - `bundle exec ruby prueba_dos.rb` 40 | 7. Utilizando el proyecto creado en el punto anterior como referencia, contestá las siguientes preguntas: 41 | 1. ¿Qué finalidad tiene el archivo `Gemfile`? 42 | 2. ¿Para qué sirve la directiva `source` del `Gemfile`? ¿Cuántas pueden haber en un mismo archivo? 43 | 3. Acorde a cómo agregaste la gema `colorputs`, ¿qué versión se instaló de la misma? Si mañana se publicara la versión 44 | `7.3.2`, ¿esta se instalaría en tu proyecto? ¿Por qué? ¿Cómo podrías limitar esto y hacer que sólo se instalen 45 | *releases* de la gema en las que no cambie la *versión mayor* de la misma? 46 | 4. ¿Qué ocurrió la primera vez que ejecutaste `prueba.rb`? ¿Por qué? 47 | 5. ¿Qué cambió al ejecutar `bundle install`? 48 | 6. ¿Qué diferencia hay entre `bundle install` y `bundle update`? 49 | 7. ¿Qué ocurrió al ejecutar `prueba_dos.rb` de las distintas formas enunciadas? ¿Por qué? ¿Cómo modificarías el 50 | archivo `prueba_dos.rb` para que funcione correctamente? 51 | 8. Desarrollá una gema (llamada `MethodCounter`, por ejemplo) que empaquete toda la funcionalidad implementada en el ejercicio 4 de la práctica 2 (el módulo `Countable`). 52 | La forma de usarla sería algo similar a esto: 53 | 54 | ```ruby 55 | require 'method_counter' 56 | 57 | class MiClase 58 | include MethodCounter::Countable 59 | 60 | def hola 61 | puts "Hola" 62 | end 63 | 64 | def chau 65 | puts "Chau" 66 | end 67 | 68 | count_invocations_of :hola, :chau 69 | end 70 | ``` 71 | 72 | ## Sinatra 73 | 74 | 1. ¿Qué es Rack? ¿Qué define? ¿Qué requisitos impone? 75 | 2. Implementá una *app* llamada "MoL" de Rack que responda con un número al azar entre `1` y `42`, y que devuelva el 76 | *status* HTTP `200` sólo en caso que el número a devolver sea `42`, en cualquier otro caso debe retornar un *status* 77 | `404`. 78 | 3. Sinatra se define como "*DSL para crear aplicaciones web*". ¿Qué quiere decir esto? ¿Qué es un *DSL*? 79 | 4. Implementá la misma *app* "MoL" de antes, ahora utilizando Sinatra para obtener el mismo resultado. 80 | 5. Utilizando Sinatra, desarrollá una aplicación web que tenga los siguientes *endpoints*: 81 | - `GET /` lista todos los endpoints disponibles (sirve a modo de documentación) 82 | - `GET /mcm/:a/:b` calcula y presenta el mínimo común múltiplo de los valores numéricos `:a` y `:b` 83 | - `GET /mcd/:a/:b` calcula y presenta el máximo común divisor de los valores numéricos `:a` y `:b` 84 | - `GET /sum/*` calcula la sumatoria de todos los valores numéricos recibidos como parámetro en el *splat* 85 | - `GET /even/*` presenta la cantidad de números pares que hay entre los valores numéricos recibidos como parámetro en 86 | el *splat* 87 | - `POST /random` presenta un número al azar 88 | - `POST /random/:lower/:upper` presenta un número al azar entre `:lower` y `:upper` (dos valores numéricos) 89 | 6. Implementá un *middleware* para Sinatra que modifique la respuesta del web server y "tache" cualquier número que 90 | aparezca en el *body* de la respuesta, cambiando cada dígito por un caracter `X`. Utilizalo en la aplicación anterior 91 | para corroborar su funcionamiento. 92 | 7. Implementá otro *middleware* para Sinatra que agregue una cabecera a la respuesta HTTP, llamada `X-Xs-Count`, cuyo 93 | valor sea la cantidad de caracteres `X` que encuentra en el *body* de la respuesta. ¿Cómo debés incluirlo en tu *app* 94 | Sinatra para que este *middleware* se ejecute **antes** que el desarrollado en el punto anterior? 95 | 8. Desarrollá una aplicación Sinatra para jugar al ahorcado. La aplicación internamente debe manejar una lista de 96 | palabras (cada una asociada a algún identificador de tu elección y a información sobre los intentos realizados para 97 | adivinar esa palabra), donde cada una representa una partida de ahorcado que puede ser jugada una sóla vez por 98 | ejecución del servidor de la aplicación web. 99 | La misma debe poseer las siguientes URLs: 100 | - `POST /` inicia una partida. Internamente tomará una palabra al azar de entre las de la lista, y luego debe 101 | redirigir (con un *redirect* HTTP) a la URL propia de la partida (utilizando el identificador de la palabra elegida) 102 | para que el jugador pueda comenzar a adivinar. 103 | - `GET /partida/:id` muestra el estado actual de la partida (letras adivinadas, intentos fallidos y cantidad de 104 | intentos restantes). Si se adivinó la palabra o no quedan más intentos, deberá reflejarse también en la 105 | - `PUT /partida/:id` acepta un parámetro por `PUT` llamado `intento` que debe contener la letra que el jugador intenta 106 | para adivinar la palabra. Internamente la aplicación chequeará si se pueden hacer más intentos en la partida, en 107 | caso afirmativo actualizará el estado de la partida, y en respuesta deberá hacer un *redirect* HTTP a la partida (a 108 | `/partida/:id`) para mostrar al jugador el estado de su partida. 109 | 110 | ## Referencias 111 | 112 | A la hora de aprender un nuevo lenguaje, una herramienta o un framework, es fundamental que te familiarices con sus 113 | APIs. Sea conocer clases base del lenguaje o parte de la herramienta que estés comenzado a utilizar, las APIs que te 114 | provea serán la forma de sacarle provecho. Si además disponés de guías o *tutoriales* para complementar y guiarte en el 115 | aprendizaje (como ocurre en el caso de Sinatra y Rails), ¡mejor aún! 116 | 117 | Por eso, te dejamos en esta sección los links para que puedas consultar la documentación de las herramientas que ves en 118 | esta práctica: 119 | 120 | * Rubygems - https://rubygems.org 121 | * [Guías](http://guides.rubygems.org/) 122 | * Bundler - http://bundler.io 123 | * [Motivación y breve ejemplo](http://bundler.io/rationale.html) 124 | * [Gemfile](http://bundler.io/gemfile.html) 125 | * Sinatra - http://sinatrarb.com/ 126 | * [APIs](http://www.rubydoc.info/gems/sinatra) 127 | * Guía rápida [en inglés](http://www.sinatrarb.com/intro.html) y [en español](http://www.sinatrarb.com/intro-es.html) 128 | * [Índice de documentación](http://www.sinatrarb.com/documentation.html) 129 | * [Presentación en español](http://www.slideshare.net/godfoca/sinatra-1282891) 130 | -------------------------------------------------------------------------------- /practicas/05-rails.md: -------------------------------------------------------------------------------- 1 | # TTPS opción Ruby 2 | 3 | # Práctica 5 4 | 5 | En esta práctica incorporaremos conceptos del framework Ruby on Rails, desarrollando las posibilidades que nos ofrece 6 | para facilitar el desarrollo ágil de aplicaciones web. 7 | 8 | ## Rails 9 | 10 | 1. El framework está compuesto de diferentes librerías: 11 | 12 | * ActionMailer 13 | * ActionPack 14 | * ActionView 15 | * ActiveJob 16 | * ActiveModel 17 | * ActiveRecord 18 | * ActiveSupport 19 | * ActionCable 20 | * ActiveStorage 21 | * ActionText 22 | * ActionMailbox 23 | 24 | Para cada una de estas librerías, analizá y respondé las siguientes preguntas: 25 | 26 | 1. ¿Qué funcion principal cumple? 27 | 2. Citá algún ejemplo donde se te ocurra que podrías utilizarla. 28 | 29 | 2. Investigá cómo se crea una aplicación Rails nueva y enumerá los pasos básicos para tener la aplicación funcionando con una 30 | base de datos SQLite. 31 | 32 | 3. Siguiendo los pasos que enumeraste en el punto anterior, creá una nueva aplicación Rails llamada `practica_cinco` 33 | en la cual vas a realizar las pruebas para los ejercicios de esta práctica. 34 | 35 | 4. ¿Qué es un **ambiente** (`environment`) en una aplicación Rails? ¿Qué sentido considerás que tiene usar diferentes 36 | ambientes en tu aplicación? ¿Cuál es el ambiente por defecto? 37 | 38 | 5. Sobre la configuración de Rails: 39 | 40 | 1. ¿De qué forma podés especificar parámetros de configuración del framework en una app Rails? 41 | 2. ¿Cuáles son los archivos principales? Intentá relacionar este concepto con los ambientes que antes viste. 42 | 3. Modificá el `locale` por defecto de tu aplicación para que sea español. 43 | 4. Modificá la zona horaria de tu aplicación para que sea la de la Argentina. 44 | 45 | 6. Sobre los *initializers*: 46 | 47 | 1. ¿Qué son y para qué se utilizan? 48 | 2. ¿En qué momento de la vida de la aplicación se ejecutan? 49 | 3. Si tu app está corriendo y modificás algún initializer, ¿los cambios se reflejan automáticamente? ¿Por qué? 50 | 4. Creá un initializer en tu aplicación que imprima en pantalla el string `"Booting practica_cinco"`. 51 | ¿En qué momento se visualiza este mensaje? 52 | 53 | 7. Sobre los *generators*: 54 | 55 | 1. ¿Qué son? ¿Qué beneficios imaginás que trae su uso? 56 | 2. ¿Con qué comando podés consultar todos los generators disponibles en tu app Rails? 57 | 3. Utilizando el generator adecuado, creá un controller llamado `PoliteController` que tenga una acción `salute` que 58 | responda con un saludo aleatorio de entre un arreglo de 5 diferentes, como por ejemplo *"Good day sir/ma'am."*. 59 | 60 | 8. Sobre *routing*: 61 | 62 | 1. ¿Dónde se definen las rutas de la app Rails? 63 | 2. ¿De qué formas se pueden definir las rutas? 64 | 3. ¿Qué ruta(s) agregó el generator que usaste antes? 65 | 4. ¿Con qué comando podés consultar todas las rutas definidas en tu app Rails? 66 | 67 | ## ActiveSupport (AS) 68 | 69 | 1. ¿De qué forma extiende AS las clases básicas de Ruby para incorporar nuevos métodos? 70 | 71 | 2. Investigá qué métodos agrega AS a las siguientes clases: 72 | 73 | 1. `String` 74 | 2. `Array` 75 | 3. `Hash` 76 | 4. `Date` 77 | 5. `Numeric` 78 | 79 | 3. ¿Qué hacen y en qué clase define AS los siguientes métodos? 80 | 81 | 1. `blank?` 82 | 2. `present?` 83 | 3. `presence` 84 | 4. `try` 85 | 5. `in?` 86 | 6. `alias_method_chain` 87 | 7. `delegate` 88 | 8. `pluralize` 89 | 9. `singularize` 90 | 10. `camelize` 91 | 11. `underscore` 92 | 12. `classify` 93 | 13. `constantize` 94 | 14. `humanize` 95 | 15. `sum` 96 | 97 | 4. ¿De qué manera se le puede *enseñar* a AS cómo pasar de singular a plural (o viceversa) los sustantivos que usamos 98 | en nuestro código? 99 | 100 | > Tip: Mirá el archivo config/initializers/inflections.rb 101 | 102 | 5. Modificá la configuración de la aplicación Rails para que *aprenda* a pluralizar correctamente en español todas las 103 | palabras que terminen en `l`, `n` y `r`. 104 | 105 | > Tip: el uso de expresiones regulares simples ayuda. :) 106 | 107 | ## ActiveRecord (AR) 108 | 109 | 1. ¿Cómo se define un modelo con ActiveRecord? ¿Qué requisitos tienen que cumplir las clases para utilizar la lógica de 110 | abstracción del acceso a la base de datos que esta librería ofrece? 111 | 2. ¿De qué forma *sabe* ActiveRecord qué campos tiene un modelo? 112 | 3. ¿Qué metodos (*getters* y *setters*) genera AR para los campos escalares básicos (`integer`, `string`, `text`, `boolean`, `float`)? 113 | 4. ¿Qué convención define AR para inferir los nombres de las tablas a partir de las clases Ruby? Citá ejemplos. 114 | 5. Sobre las migraciones de AR: 115 | 1. ¿Qué son y para qué sirven? 116 | 2. ¿Cómo se generan? 117 | 3. ¿Dónde se guardan en el proyecto? 118 | 4. ¿Qué organización tienen sus nombres de archivo? 119 | 5. Generá una migración que cree la tabla `offices` con los siguientes atributos: 120 | * `name`: `string` de `255` caracteres, no puede ser nulo. 121 | * `phone_number`: `string` de `30` caracteres, no puede ser nulo. 122 | * `address`: `text`. 123 | * `available`: `boolean`, por defecto `false`, no puede ser nulo. 124 | 6. Creá el modelo `Office` para la tabla `offices` que creaste antes, e implementale el método `#to_s`. 125 | 7. Utilizando migraciones, creá la tabla y el modelo para la clase `Employee`, con la siguiente estructura: 126 | * `name`: `string` de `150` caracteres, no puede ser nulo. 127 | * `e_number`: `integer`, no puede ser nulo, debe ser único. 128 | * `office_id`: `integer`, foreign key hacia `offices`. 129 | 8. Agregá una asociación entre `Employee` y `Office` acorde a la columna `office_id` que posee la tabla `employees`. 130 | 1. ¿Qué tipo de asociación declaraste en la clase `Employee`? 131 | 2. ¿Y en la clase `Office`? 132 | 3. ¿Qué métodos generó AR en el modelo a partir de esto? 133 | 4. Modificá el mapeo de rutas de tu aplicación Rails para que al acceder a `/` se vaya al controller definido antes (`polite#salute`). 134 | 9. Sobre *scopes*: 135 | 1. ¿Qué son los scopes de AR? ¿Para qué los utilizarías? 136 | 2. Investigá qué diferencia principal existe entre un método estático y un scope. 137 | 3. Agregá los siguientes scopes al modelo `Employee`: 138 | * `vacant`: Filtra los empleados para quedarse únicamente con aquellos que no tengan una oficina asignada (*asociada*). 139 | * `occupied`: Complemento del anterior, devuelve los empleados que sí tengan una oficina asignada. 140 | 4. Agregá este scope al modelo `Office`: 141 | * `empty`: Devuelve las oficinas que están disponibles (`available = true`) que no tienen empleados asignados. 142 | 10. Sobre *scaffold controllers*: 143 | 1. ¿Qué son? ¿Qué operaciones proveen sobre un modelo? 144 | 2. ¿Con qué comando se generan? 145 | 3. Utilizando el generator anterior, generá un controlador de scaffold para el modelo `Office` y otro para el modelo 146 | `Employee`. 147 | 4. ¿Qué rutas agregó este generator? 148 | 5. Analizá el código que se te generó para los controllers y para las vistas, y luego modificalo para que no permita 149 | el borrado de ninguno de los elementos. ¿Qué cambios debés hacer para que las vistas no muestren la opción, el 150 | controller no tenga la acción `destroy` y las rutas de borrado dejen de existir en la aplicación? 151 | 152 | ## ActiveModel (AM) 153 | 154 | 1. ¿Qué son los validadores de AM? 155 | 2. Agregá a los modelos `Office` y `Employee` las validaciones necesarias para hacer que sus atributos cumplan las 156 | restricciones definidas para las columnas de la tabla que cada uno representa. 157 | 3. Validadores personalizados: 158 | 1. ¿Cómo podés definir uno para tus modelos AR? 159 | 2. Implementá un validador que chequee que un string sea un número telefónico con un formato válido para la 160 | Argentina. 161 | 3. Agregá el validador que definiste en el punto anterior a tu modelo `Office` para validar el campo `phone_number`. 162 | 163 | ## Internacionalización (i18n) y localización (l10n) 164 | 165 | 1. ¿A qué hacen referencia estos conceptos? 166 | 167 | 2. Investigá qué dos metodos provee la clase `I18n` para realizar la traducción (i18n) y la localización (l10n). 168 | 169 | 3. Modificá el controller `PoliteController` que creaste antes para que utilice traducciones al imprimir el mensaje 170 | de saludo. 171 | 172 | 4. Modificá los controllers de scaffold que generaste para que utilicen i18n, tanto en las vistas como en los mensajes 173 | flash del controller. 174 | 175 | > Tip: Investigá qué helper provee Rails en las vistas para las traducciones. 176 | 177 | ## Referencias 178 | 179 | * Ruby on Rails - http://rubyonrails.org 180 | * [APIs](http://api.rubyonrails.org/) 181 | * [Guías](http://guides.rubyonrails.org/) 182 | * [Guía básica de ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) 183 | * [Extensiones de ActiveSupport](http://guides.rubyonrails.org/active_support_core_extensions.html) 184 | * [Rails for Zombies](http://railsforzombies.org/) (Un poco desactualizado, pero vale la pena mencionarlo) 185 | -------------------------------------------------------------------------------- /practicas/03-excepciones-testing.md: -------------------------------------------------------------------------------- 1 | # TTPS opción Ruby 2 | 3 | # Práctica 3 4 | 5 | Esta práctica incorpora ejercicios sobre excepciones, tanto definición y manejo de las misma como un breve sondeo de las 6 | principales clases de excepción, y a su vez introduce algunos ejercicios sobre testing en Ruby. 7 | 8 | ## Excepciones 9 | 10 | 1. Investigá la jerarquía de clases que presenta Ruby para las excepciones. ¿Para qué se utilizan las siguientes clases? 11 | * `IOError` 12 | * `NameError` 13 | * `RuntimeError` 14 | * `NotImplementedError` 15 | * `StopIteration` 16 | * `TypeError` 17 | * `SystemExit` 18 | * `SystemStackError` 19 | * `ZeroDivisionError` 20 | * `StandardError` 21 | * `ArgumentError` 22 | 2. ¿Cuál es la diferencia entre `raise` y `throw`? ¿Para qué usarías una u otra opción? 23 | 3. ¿Para qué sirven `begin .. rescue .. else` y `ensure`? Pensá al menos 2 casos concretos en que usarías estas 24 | sentencias en un script Ruby. 25 | 4. ¿Para qué sirve `retry`? ¿Cómo evitarías caer en un loop infinito al usarla? 26 | 5. ¿Cuáles son las diferencias entre los siguientes métodos? 27 | ```ruby 28 | def opcion_1 29 | a = [1, nil, 3, nil, 5, nil, 7, nil, 9, nil] 30 | b = 3 31 | c = a.map do |x| 32 | x * b 33 | end 34 | puts c.inspect 35 | rescue 36 | 0 37 | end 38 | 39 | def opcion_2 40 | c = begin 41 | a = [1, nil, 3, nil, 5, nil, 7, nil, 9, nil] 42 | b = 3 43 | a.map do |x| 44 | x * b 45 | end 46 | rescue 47 | 0 48 | end 49 | puts c.inspect 50 | end 51 | 52 | def opcion_3 53 | a = [1, nil, 3, nil, 5, nil, 7, nil, 9, nil] 54 | b = 3 55 | c = a.map { |x| x * b } rescue 0 56 | puts c.inspect 57 | end 58 | 59 | def opcion_4 60 | a = [1, nil, 3, nil, 5, nil, 7, nil, 9, nil] 61 | b = 3 62 | c = a.map { |x| x * b rescue 0 } 63 | puts c.inspect 64 | end 65 | ``` 66 | 6. Suponé que tenés el siguiente script y se te pide que lo hagas _resiliente_ (tolerante a fallos), 67 |   intentando siempre que se pueda recuperar la situación y volver a intentar la operación que falló. Realizá las 68 | modificaciones que consideres necesarias para lograr que el script sea más robusto. 69 | 70 | ```ruby 71 | # Este script lee una secuencia de no menos de 15 números desde teclado y luego imprime el resultado de la división 72 | # de cada número por su entero inmediato anterior. 73 | 74 | # Como primer paso se pide al usuario que indique la cantidad de números que ingresará. 75 | cantidad = 0 76 | while cantidad < 15 77 | puts '¿Cuál es la cantidad de números que ingresará? Debe ser al menos 15' 78 | cantidad = gets.to_i 79 | end 80 | 81 | # Luego se almacenan los números 82 | numeros = 1.upto(cantidad).map do 83 | puts 'Ingrese un número' 84 | numero = gets.to_i 85 | end 86 | 87 | # Y finalmente se imprime cada número dividido por su número entero inmediato anterior 88 | resultado = numeros.map { |x| x / (x - 1) } 89 | puts 'El resultado es: %s' % resultado.join(', ') 90 | ``` 91 | 7. Partiendo del script del inciso anterior, implementá una nueva clase de excepción que se utilizará para indicar que 92 | la entrada del usuario no es un valor numérico entero válido. ¿De qué clase de la jerarquía de `Exception` heredaría? 93 | 8. Sea el siguiente código: 94 | ```ruby 95 | def fun3 96 | puts "Entrando a fun3" 97 | raise RuntimeError, "Excepción intencional" 98 | puts "Terminando fun3" 99 | rescue NoMethodError => e 100 | puts "Tratando excepción por falta de método" 101 | rescue RuntimeError => e 102 | puts "Tratando excepción provocada en tiempo de ejecución" 103 | rescue 104 | puts "Tratando una excepción cualquiera" 105 | ensure 106 | puts "Ejecutando ensure de fun3" 107 | end 108 | 109 | def fun2(x) 110 | puts "Entrando a fun2" 111 | fun3 112 | a = 5 / x 113 | puts "Terminando fun2" 114 | end 115 | 116 | def fun1(x) 117 | puts "Entrando a fun1" 118 | fun2 x 119 | rescue 120 | puts "Manejador de excepciones de fun1" 121 | raise 122 | ensure 123 | puts "Ejecutando ensure de fun1" 124 | end 125 | 126 | begin 127 | x = 0 128 | begin 129 | fun1 x 130 | rescue Exception => e 131 | puts "Manejador de excepciones de Main" 132 | if x == 0 133 | puts "Corrección de x" 134 | x = 1 135 | retry 136 | end 137 | end 138 | puts "Salida" 139 | end 140 | ``` 141 | 1. Seguí el flujo de ejecución registrando la traza de impresiones que deja el programa, analizando por qué partes 142 | del código va pasando y justificando esos pasos. 143 | 2. ¿Qué pasaría si se permuta, dentro de `fun3`, el manejador de excepciones para `RuntimeError` y el manejador de 144 | excepciones genérico (el que tiene el rescue vacío)? 145 | 3. ¿El uso de `retry` afectaría el funcionamiento del programa si se mueve la línea `x = 0` dentro del segundo 146 | `begin` (inmediatamente antes de llamar a `fun1` con `x`)? 147 | 148 | ## Testing 149 | 150 | > Nota: Para esta práctica utilizaremos `MiniTest` en cualquiera de sus variantes (`minitest/unit` o `minitest/spec`). 151 | 152 | 9. ¿En qué consiste la metodología TDD? ¿En qué se diferencia con la forma tradicional de escribir código y luego 153 | realizar los tests? 154 | 10. Dado los siguientes tests, escribí el método correspondiente (el que se invoca en cada uno) para hacer que pasen: 155 | ```ruby 156 | require 'minitest/autorun' 157 | require 'minitest/spec' 158 | 159 | describe '#incrementar' do 160 | describe 'cuando el valor es numérico' do 161 | it 'incrementa el valor en un delta recibido por parámetro' do 162 | x = -9 163 | delta = 10 164 | assert_equal(1, incrementar(x, delta)) 165 | end 166 | 167 | it 'incrementa el valor en un delta de 1 unidad por defecto' do 168 | x = 10 169 | assert_equal(11, incrementar(x)) 170 | end 171 | end 172 | 173 | describe 'cuando el valor es un string' do 174 | it 'arroja un RuntimeError' do 175 | x = '10' 176 | assert_raises(RuntimeError) do 177 | incrementar(x) 178 | end 179 | assert_raises(RuntimeError) do 180 | incrementar(x, 9) 181 | end 182 | end 183 | end 184 | end 185 | 186 | describe '#concatenar' do 187 | it 'concatena todos los parámetros que recibe en un string, separando por espacios' do 188 | class Dummies; end 189 | 190 | assert_equal('Lorem ipsum 4 Dummies', concatenar('Lorem', :ipsum, 4, Dummies)) 191 | end 192 | 193 | it 'Elimina dobles espacios si los hubiera en la salida final' do 194 | assert_equal('TTPS Ruby', concatenar('TTPS', nil, ' ', "\t", "\n", 'Ruby')) 195 | end 196 | end 197 | ``` 198 | 11. Implementá al menos 3 tests para cada uno de los siguientes ejercicios de las prácticas anteriores: 199 | 1. De la práctica 1: 4 (`en_palabras`), 5 (`contar`), 6 (`contar_palabras`) y 9 (`longitud`). 200 | 2. De la práctica 2: 1 (`ordenar_arreglo`), 2 (`ordenar`), 4 (`longitud`), 14 (`opposite`) y 16 (`da_nil?`). 201 | 12. Implementá los tests que consideres necesarios para probar el _Mixin_ `Countable` que desarrollaste en el ejercicio 202 | 11 de la práctica 2, sin dejar de cubrir los siguientes puntos: 203 | * Testear en una clase existente 204 | * Testear en una clase creada únicamente con el propósito de testear 205 | * Testear qué ocurre antes de que se invoque el método del que se está contando las invocaciones 206 | * Testear la inicialización correcta del _Mixin_ 207 | * Testear algún caso extremo que se te ocurra 208 | 13. Suponé que tenés que desarrollar una función llamada 'expansor' la cual recibe un string (conformado únicamente con 209 | letras) y devuelve otro string donde cada letra aparezca tantas veces según su lugar en el abecedario. 210 | Un ejemplo simple sería: 211 | ```ruby 212 | expansor 'abcd' 213 | # => 'abbcccdddd' 214 | ``` 215 | 216 | A continuación se presentará su especificación (sin implementar): 217 | 218 | ```ruby 219 | require 'minitest/autorun' 220 | require 'minitest/spec' 221 | 222 | describe 'expansor' do 223 | # Casos de prueba con situaciones y/o entradas de datos esperadas 224 | describe 'Casos felices' do 225 | describe 'cuando la entrada es el string "a"' do 226 | it 'debe devolver "a"' 227 | end 228 | 229 | describe 'cuando la entrada es el string "f"' do 230 | it 'debe devolver "ffffff"' 231 | end 232 | 233 | describe 'cuando la entrada es el string "escoba"' do 234 | it 'debe devolver "eeeeessssssssssssssssssscccooooooooooooooobba"' 235 | end 236 | end 237 | 238 | # Casos de pruebas sobre situaciones inesperadas y/o entradas de datos anómalas 239 | describe 'Casos tristes' do 240 | describe 'cuando la entrada no es un string' do 241 | it 'debe disparar una excepción estándar con el mensaje "La entrada no es un string"' 242 | end 243 | 244 | describe 'cuando la entrada es el string vacío' do 245 | it 'debe disparar una excepción estándar con el mensaje "El string es vacío"' 246 | end 247 | 248 | describe 'cuando la entrada es el string "9"' do 249 | it 'debe disparar un excepción estándar con el mensaje "El formato del string es incorrecto"' 250 | end 251 | 252 | describe 'cuando la entrada es el string "*"' do 253 | it 'debe disparar una excepción estándar con el mensaje "El formato del string es incorrecto"' 254 | end 255 | end 256 | end 257 | ``` 258 | 259 | 1. Completar la especificación de los casos de prueba. 260 | 2. Implementar la función `expansor` y verificar que todos los casos pasen. 261 | 262 | -------------------------------------------------------------------------------- /practicas/01-intro-git-ruby.md: -------------------------------------------------------------------------------- 1 | # TTPS opción Ruby 2 | 3 | # Práctica 1 4 | 5 | En esta práctica inicial del taller incorporaremos algunos conceptos de Git, la herramienta de control de versiones que 6 | utilizaremos a lo largo de la materia, y comenzaremos a tener contacto con el lenguaje de programación Ruby. 7 | 8 | > Nota: para simplificar, todos los comandos que se muestran en esta práctica asumen un sistema operativo Ubuntu. 9 | 10 | ## I. Repaso de Git 11 | 12 | ### Prerequisitos 13 | 14 | Para realizar esta parte de la práctica (y el resto de la materia también) necesitás tener Git instalado en la 15 | computadora donde vayas a hacer los ejercicios. Para saber si tenés Git instalado, podés ejecutar el comando `git` en 16 | una terminal y analizar la salida: 17 | 18 | ```console 19 | $ git --version 20 | git version 2.17.1 21 | ``` 22 | 23 | > En esta y todas las prácticas, cuando estemos hablando de ejecutar comandos en una terminal vamos a denotar las líneas 24 | > que tenés que ejecutar con un símbolo de prompt `$` si debés ejecutarlo con tu usuario o `#` si debés hacerlo con un 25 | > usuario con privilegios de administrador (típicamente referenciado como el usuario `root`). 26 | 27 | En el ejemplo anterior ejecutamos el comando `git --version` y obtuvimos la salida `git version 2.17.1`, lo cual indica 28 | que tenemos la versión `2.17.1` instalada y ya estamos listos para realizar los ejercicios de esta parte de la práctica. 29 | 30 | Si al ejecutarlo recibiste un mensaje de error indicando que el comando `git` no fue encontrado, eso quiere decir que 31 | Git no está instalado en tu computadora, y por ende debés instalarlo ejecutando el siguiente comando: 32 | 33 | ```console 34 | # apt update -qq && apt install -y git 35 | ``` 36 | 37 | > Nota: el comando `apt` está disponible en versiones recientes de Ubuntu. Si al ejecutar esto obtenés un error por 38 | > comando desconocido, reemplazá `apt` por `apt-get` en los comandos anteriores. 39 | 40 | Una vez finalizada la instalación el comando `git` estará disponible para que lo uses. 41 | 42 | ### Subcomandos Git 43 | 44 | Git se maneja desde la línea de comandos mediante el uso de _subcomandos_. Cada tarea, orden o consulta que quieras 45 | hacer con Git la vas a poder realizar con el comando `git` y alguno de sus subcomandos, los cuales podés listar al 46 | ejecutar (solo) `git` o uno de los subcomandos más útiles que tiene Git: `git help`. 47 | 48 | ### Cómo obtener ayuda 49 | 50 | Git provee ayuda mediante las páginas del manual (o _man pages_) para cualquier subcomando que ofrezca, y para hacer aún 51 | más fácil la consulta de esa ayuda provee un subcomando especial que nos abre la página del manual del subcomando que 52 | querramos: `git help`. 53 | 54 | ### Ejercicios 55 | 56 | 1. Ejecutá `git` o `git help` en la línea de comandos y mirá los subcomandos que tenés disponibles. 57 | 2. Ejecutá el comando `git help help`. ¿Cuál fue el resultado? 58 | 3. Utilizá el subcomando `help` para conocer qué opción se puede pasar al subcomando `add` para que ignore errores al 59 | agregar archivos. 60 | 4. ¿Cuáles son los estados posibles en Git para un archivo? ¿Qué significa cada uno? 61 | 5. Cloná el repositorio de materiales de la materia: `https://github.com/TTPS-ruby/capacitacion-ruby-ttps.git`. Una vez 62 | finalizado, ¿cuál es el _hash_ del último commit que hay en el repositorio que clonaste? 63 | 64 | > Tip: `git log`. 65 | 6. ¿Para qué se utilizan los siguientes subcomandos? 66 | * `init` 67 | * `status` 68 | * `log` 69 | * `fetch` 70 | * `merge` 71 | * `pull` 72 | * `commit` 73 | * `stash` 74 | * `push` 75 | * `rm` 76 | * `checkout` 77 | 7. Creá un archivo de texto en el repositorio que clonaste en el ejercicio **5** y verificá el estado de tu espacio de 78 | trabajo con el subcomando `status`. ¿En qué estado está el archivo que agregaste? 79 | 8. Utilizá el subcomando `log` para ver los commits que se han hecho en el repositorio, tomá cualquiera de ellos y copiá 80 | su _hash_ (por ejemplo, `800dcba6c8bb2881d90dd39c285a81eabee5effa`), y luego utilizá el subcomando `checkout` para 81 | _viajar en el tiempo_ (apuntar tu copia local) a ese commit. ¿Qué commits muestra ahora `git log`? ¿Qué ocurrió con 82 | los commits que no aparecen? ¿Qué dice el subcomando `status`? 83 | 9. Volvé al último commit de la rama principal (`master`) usando nuevamente el subcomando `checkout`. 84 | Corroborá que efectivamente haya ocurrido esto. 85 | 10. Creá un directorio vacío en el raiz del proyecto clonado. ¿En qué estado aparece en el `git status`? ¿Por qué? 86 | 11. Creá un archivo vacío dentro del directorio que creaste en el ejercicio anterior y volvé a ejecutar el subcomando 87 | `status`. ¿Qué ocurre ahora? ¿Por qué? 88 | 12. Utilizá el subcomando `clean` para eliminar los archivos no versionados (_untracked_) y luego ejecutá `git status`. 89 | ¿Qué información muestra ahora? 90 | 13. Actualizá el contenido de tu copia local mediante el subcomando `pull`. 91 | 92 | 93 | ## II. Ruby: sintaxis y tipos básicos 94 | 95 | ### Prerequisitos 96 | 97 | Antes de realizar los ejercicios de esta parte, necesitás tener instalada la última versión de Ruby en tu computadora. 98 | Al momento de publicar esta práctica la última versión estable de Ruby es la `2.6.3`, por lo que instalaremos esa 99 | versión utilizando [Rbenv](https://github.com/sstephenson/rbenv#installation) y su _plugin_ 100 | [ruby-build](https://github.com/sstephenson/ruby-build#installation). Rbenv permite usar distintas versiones de Ruby en 101 | nuestra computadora, y ruby-build es una extensión de Rbenv que simplifica la instalación de las versiones del lenguaje. 102 | 103 | > Los pasos para instalar todas las partes que detallamos a continuación son un resumen rápido de los pasos que debés 104 | > realizar para tener un ambiente de Ruby funcionando en tu computadora. Aquí obviaremos los detalles de cada paso, por 105 | > lo que te recomendamos que leas los `README` de las dos herramientas que antes mencionamos para conocer más en 106 | > profundidad cómo funcionan. 107 | 108 | Ejecutá la siguiente secuencia de comandos para instalar Ruby 2.6.3 en tu computadora: 109 | 110 | ```console 111 | # apt install -y autoconf bison build-essential lib{ssl,yaml,sqlite3}-dev libreadline{7,-dev} zlib1g{,-dev} 112 | $ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv 113 | $ cd ~/.rbenv && src/configure && make -C src 114 | $ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build 115 | $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc 116 | $ echo 'eval "$(rbenv init -)"' >> ~/.bashrc 117 | $ source ~/.bashrc 118 | $ rbenv install 2.6.3 119 | $ rbenv global 2.6.3 120 | ``` 121 | 122 | Si no disponés de un equipo con GNU/Linux o no querés realizar la instalación en tu sistema operativo habitual, 123 | desde la cátedra hemos preparado un box de [Vagrant](https://vagrantup.com) para utilizar con [Virtualbox](https://virtualbox.org) 124 | que contiene una instalación lista (con los pasos mencionados antes) de Ruby 2.6.3 y git. Los pasos para poder instalar 125 | las dependencias y utilizarla están descriptos junto al manifesto `Vagrantfile`, que indica cómo preparar la máquina 126 | virtual, accesibles en el [repositorio de prácticas de la materia](https://github.com/TTPS-ruby/practicas-ruby-ttps/tree/master/vagrant). 127 | Es indistinto de qué forma instales y utilices Ruby, lo importante es que utilices una versión _relativamente_ reciente (2.4.x o posterior). 128 | 129 | ### Ejercicios 130 | 131 | > Nota: al realizar estos ejercicios no utilices las sentencias de control `while`, `for` ni `repeat`. 132 | 133 | 1. Investigá y probá en un intérprete de Ruby cómo crear objetos de los siguientes tipos básicos usando literales y 134 | usando el constructor `new` (cuando sea posible): 135 | * Arreglo (`Array`) 136 | * Diccionario o _hash_ (`Hash`) 137 | * String (`String`) 138 | * Símbolo (`Symbol`) 139 | 2. ¿Qué devuelve la siguiente comparación? ¿Por qué? 140 | 141 | ```ruby 142 | 'TTPS Ruby'.object_id == 'TTPS Ruby'.object_id 143 | ``` 144 | 3. Escribí una función llamada `reemplazar` que reciba un `String` y que busque y reemplace en el mismo cualquier 145 | ocurrencia de `{` por `do\n` y cualquier ocurrencia de `}` por `\nend`, de modo que convierta los bloques escritos 146 | con llaves por bloques multilínea con `do` y `end`. Por ejemplo: 147 | 148 | ```ruby 149 | reemplazar("3.times { |i| puts i }") 150 | # => "3.times do\n |i| puts i \nend" 151 | ``` 152 | 4. Escribí una función que convierta a palabras la hora actual, dividiendo en los siguientes rangos los minutos: 153 | * Si el minuto está entre 0 y 10, debe decir "en punto", 154 | * si el minuto está entre 11 y 20, debe decir "y cuarto", 155 | * si el minuto está entre 21 y 34, debe decir "y media", 156 | * si el minuto está entre 35 y 44, debe decir "menos veinticinco" (de la hora siguiente), 157 | * si el minuto está entre 45 y 55, debe decir "menos cuarto" (de la hora siguiente), 158 | * y si el minuto está entre 56 y 59, debe decir "casi las" (y la hora siguiente) 159 | 160 | Tomá como ejemplos los siguientes casos: 161 | 162 | ```ruby 163 | # A las 10:01 164 | en_palabras(Time.now) 165 | # => "Son las 10 en punto" 166 | # A las 9:33 167 | en_palabras(Time.now) 168 | # => "Son las 9 y media" 169 | # A las 9:45 170 | en_palabras(Time.now) 171 | # => "Son las 10 menos cuarto" 172 | # A las 6:58 173 | en_palabras(Time.now) 174 | # => "Casi son las 7" 175 | ``` 176 | 177 | > Tip: resolver utilizando rangos numéricos 178 | 5. Escribí una función llamada `contar` que reciba como parámetro dos `string` y que retorne la cantidad de veces que 179 | aparece el segundo `string` en el primero, sin importar mayúsculas y minúsculas. Por ejemplo: 180 | 181 | ```ruby 182 | contar("La casa de la esquina tiene la puerta roja y la ventana blanca.", "la") 183 | # => 5 184 | ``` 185 | 6. Modificá la función anterior para que sólo considere como aparición del segundo `string` cuando se trate de palabras 186 | completas. Por ejemplo: 187 | 188 | ```ruby 189 | contar_palabras("La casa de la esquina tiene la puerta roja y la ventana blanca.", "la") 190 | # => 4 191 | ``` 192 | 7. Dada una cadena cualquiera, y utilizando los métodos que provee la clase `String`, realizá las siguientes 193 | operaciones sobre el `string`: 194 | * Imprimilo con sus caracteres en orden inverso. 195 | * Eliminá los espacios en blanco que contenga. 196 | * Convertí cada uno de sus caracteres por su correspondiente valor ASCII. 197 | * Cambiá las vocales por números (`a` por `4`, `e` por `3`, `i` por `1`, `o` por `0`, `u` por `6`). 198 | 8. ¿Qué hace el siguiente código? 199 | 200 | ```ruby 201 | [:upcase, :downcase, :capitalize, :swapcase].map do |meth| 202 | "TTPS Ruby".send(meth) 203 | end 204 | ``` 205 | 9. Escribí una función que dado un arreglo que contenga varios `string` cualesquiera, retorne un nuevo arreglo donde 206 | cada elemento es la longitud del `string` que se encuentra en la misma posición del arreglo recibido como parámetro. 207 | Por ejemplo: 208 | 209 | ```ruby 210 | longitud(['TTPS', 'Opción', 'Ruby', 'Cursada 2019']) 211 | # => [4, 6, 4, 12] 212 | ``` 213 | 10. Escribí una función llamada `a_ul` que reciba un `Hash` y retorne un `String` con los pares de clave/valor del hash 214 | formateados en una lista HTML `