diff --git a/01_inventory/Gemfile b/01_inventory/Gemfile new file mode 100644 index 0000000..98ff2c0 --- /dev/null +++ b/01_inventory/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' +gem "sinatra" +gem "rspec" + +gem 'activerecord' +gem 'activesupport' +gem 'sqlite3' diff --git a/01_inventory/Gemfile.lock b/01_inventory/Gemfile.lock new file mode 100644 index 0000000..ac25061 --- /dev/null +++ b/01_inventory/Gemfile.lock @@ -0,0 +1,57 @@ +GEM + remote: https://rubygems.org/ + specs: + activemodel (4.2.6) + activesupport (= 4.2.6) + builder (~> 3.1) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) + arel (~> 6.0) + activesupport (4.2.6) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + arel (6.0.3) + builder (3.2.2) + diff-lcs (1.2.5) + i18n (0.7.0) + json (1.8.3) + minitest (5.9.0) + rack (1.6.4) + rack-protection (1.5.3) + rack + rspec (3.2.0) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-core (3.2.0) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.1) + sinatra (1.4.7) + rack (~> 1.5) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + sqlite3 (1.3.11) + thread_safe (0.3.5) + tilt (1.3.3) + tzinfo (1.2.2) + thread_safe (~> 0.1) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord + activesupport + rspec + sinatra + sqlite3 diff --git a/01_inventory/Rakefile b/01_inventory/Rakefile new file mode 100644 index 0000000..fdd7d5d --- /dev/null +++ b/01_inventory/Rakefile @@ -0,0 +1,10 @@ +require "active_record" +require "active_support/all" + +namespace :db do + desc "migrate your database" + task :migrate do + require "./store/db_store/config" + ActiveRecord::Migrator.migrate('store/db_store/migrate') + end +end diff --git a/01_inventory/app.rb b/01_inventory/app.rb new file mode 100644 index 0000000..573103d --- /dev/null +++ b/01_inventory/app.rb @@ -0,0 +1,42 @@ +require "sinatra" +require_relative "lib/inventory" +require_relative "store/db_store/config" +require_relative "store/db_store" + +store = DbStore.new +inventory = Inventory.new(store) + +get '/' do + @articles = inventory.articles_list + erb :index +end + +get '/articles/new' do + @form = inventory.new_article_form + erb :new_article +end + +post '/articles' do + status = inventory.add_article(params) + + if status.success? + redirect "/" + else + @form = status.form_with_errors + erb :new_article + end +end + +post '/articles/:code/increment' do + inventory.increment_article_quantity(params[:code]) + redirect "/" +end + +post '/articles/:code/decrement' do + inventory.decrement_article_quantity(params[:code]) + redirect "/" +end + +after do + ActiveRecord::Base.clear_active_connections! +end diff --git a/01_inventory/lib/inventory.rb b/01_inventory/lib/inventory.rb new file mode 100644 index 0000000..0b2beb9 --- /dev/null +++ b/01_inventory/lib/inventory.rb @@ -0,0 +1,172 @@ +require "ostruct" + +class Inventory + def initialize(store) + @store = store + end + + def articles_list + store.all_articles.map { |raw| Article.new(raw) } + end + + def new_article_form + ArticleForm.new(Article.new) + end + + def add_article(params) + article = Article.new(params) + errors = ArticleValidator.new(article, store).validate! + + if errors.empty? + store.create(article.to_h) + SuccessArticleStatus.new + else + ErrorArticleStatus.new(ArticleForm.new(article, errors)) + end + end + + def increment_article_quantity(code) + article = Article.new(store.find_with_code(code)) + article.increment_quantity! + store.update(article.to_h) + end + + def decrement_article_quantity(code) + article = Article.new(store.find_with_code(code)) + article.decrement_quantity! do |article| + store.update(article.to_h) + end + end + + private + + attr_reader :store +end + +module Presence + def present?(attr) + !attr.nil? && attr != "" + end +end + +module Delegate + def delegate(*keys, to:, sufix: false) + keys.each do |key| + method_name = sufix && "#{key}_#{to}" || key + define_method method_name do + self.send(to).send(key) + end + end + end +end + +class ArticleValidator + include Presence + + def initialize(article, store) + @store = store + @article = article + end + + def validate! + self.errors = {} + validate_presence_of! :name, :code, :quantity + validate_uniqness_of_code! + validate_quantity_is_greater_than_or_equals_than_zero! + errors + end + + private + + attr_reader :article, :store + attr_accessor :errors + + def validate_presence_of!(*attr_keys) + attr_keys.each do |attr_key| + unless present?(article.send(attr_key)) + errors[attr_key] = "can't be blank" + end + end + end + + def validate_uniqness_of_code! + if present?(article.code) && store.find_with_code(article.code) + errors[:code] = "already taken" + end + end + + def validate_quantity_is_greater_than_or_equals_than_zero! + if present?(article.quantity) && article.quantity < 0 + errors[:quantity] = "should be greater or equals than 0" + end + end +end + +class ArticleForm + extend Delegate + + delegate :name, :code, :quantity, to: :article + delegate :name, :code, :quantity, to: :errors, sufix: true + + def initialize(article, errors = {}) + @article = article + @errors = OpenStruct.new(errors) + end + + private + + attr_reader :article, :errors +end + +class ErrorArticleStatus + attr_reader :form_with_errors + + def initialize(form) + @form_with_errors = form + end + + def success? + false + end +end + +class SuccessArticleStatus + def success? + true + end +end + +class Article + include Presence + + attr_reader :name, :code, :quantity + + def initialize(data = {}) + @name = data["name"] + @code = data["code"] + self.quantity = data["quantity"] + end + + def to_h + {"name" => name, + "code" => code, + "quantity" => quantity} + end + + def increment_quantity! + self.quantity = quantity + 1 + end + + def decrement_quantity!(&block) + if quantity > 0 + self.quantity = quantity - 1 + block.call(self) + end + end + + private + + def quantity=(value) + @quantity = value.to_i if present?(value) + end +end diff --git a/01_inventory/spec/inventory_spec.rb b/01_inventory/spec/inventory_spec.rb index cf987cb..6e02f75 100644 --- a/01_inventory/spec/inventory_spec.rb +++ b/01_inventory/spec/inventory_spec.rb @@ -1,9 +1,167 @@ require "rspec" +require_relative "../lib/inventory" +require_relative "../store/in_memory_store" RSpec.describe "Inventory" do describe "shows the articles" do - it "with name" - it "with code" - it "with quantity" + attr_reader :articles + + before do + inventory = Inventory.new(store_with([ + {"name" => "Camisa 1", "code" => "c1", "quantity" => 10}, + {"name" => "Gorra 1", "code" => "g1", "quantity" => 35} + ])) + + @articles = inventory.articles_list + end + + it "with name" do + expect(articles.map(&:name)).to eq ["Camisa 1", "Gorra 1"] + end + + it "with code" do + expect(articles.map(&:code)).to eq ["c1", "g1"] + end + + it "with quantity" do + expect(articles.map(&:quantity)).to eq [10, 35] + end + end + + describe "adds article" do + attr_reader :store, :inventory, :good_params + + before do + @store = store_with([]) + @inventory = Inventory.new(store) + @good_params = { + "name" => "Camisa 1", + "code" => "c1", + "quantity" => "10" + } + end + + it "with name, code and quantity" do + expect(store).to receive(:create).with(good_params.merge("quantity" => 10)) + inventory.add_article(good_params) + end + + it "returns success when params are good" do + status = inventory.add_article(good_params) + expect(status).to be_success + end + + it "has a new empty article form" do + form = inventory.new_article_form + expect(form.name).to be_nil + expect(form.name_errors).to be_nil + expect(form.code).to be_nil + expect(form.quantity).to be_nil + end + + it "on error does not return success" do + params = good_params.merge("name" => nil) + status = inventory.add_article(params) + expect(status).not_to be_success + end + + it "on error does not create the article" do + params = good_params.merge("name" => nil) + expect(store).not_to receive(:create).with(params) + inventory.add_article(params) + end + + it "on error returns a form with the current values" do + params = good_params.merge("name" => nil) + status = inventory.add_article(params) + form = status.form_with_errors + expect(form.name).to eq nil + expect(form.code).to eq "c1" + expect(form.quantity).to eq 10 + end + + [nil, ""].each do |blank| + it "validates presence of name" do + params = good_params.merge("name" => blank) + status = inventory.add_article(params) + form = status.form_with_errors + expect(form.name_errors).to eq "can't be blank" + end + end + + [nil, ""].each do |blank| + it "validates presence of code" do + params = good_params.merge("code" => blank) + status = inventory.add_article(params) + form = status.form_with_errors + expect(form.code_errors).to eq "can't be blank" + end + end + + [nil, ""].each do |blank| + it "validates presence of quantity" do + params = good_params.merge("quantity" => blank) + status = inventory.add_article(params) + form = status.form_with_errors + expect(form.quantity_errors).to eq "can't be blank" + end + end + + it "validates quantity is greater or equals than 0" do + params = good_params.merge("quantity" => -1) + status = inventory.add_article(params) + form = status.form_with_errors + expect(form.quantity_errors).to eq "should be greater or equals than 0" + end + + it "validates that the code is unique" do + params = good_params.merge("code" => "c1") + status = inventory.add_article(params) + expect(status).to be_success + + status = inventory.add_article(params) + form = status.form_with_errors + expect(form.code_errors).to eq "already taken" + end + end + + describe "modifies articles quantity" do + attr_reader :store, :inventory + + before do + @store = store_with([ + {"name" => "Camisa 1", "code" => "c1", "quantity" => 10}, + {"name" => "Gorra 1", "code" => "g1", "quantity" => 0}]) + @inventory = Inventory.new(store) + end + + it "incrementing it" do + expect(store). + to receive(:update). + with({ + "name" => "Camisa 1", + "code" => "c1", + "quantity" => 11}) + inventory.increment_article_quantity("c1") + end + + it "decrementing it" do + expect(store). + to receive(:update). + with({ + "name" => "Camisa 1", + "code" => "c1", + "quantity" => 9}) + inventory.decrement_article_quantity("c1") + end + + it "decrementing it (unless is 0)" do + expect(store).not_to receive(:update) + inventory.decrement_article_quantity("g1") + end + end + + def store_with(records) + InMemoryStore.new(records) end end diff --git a/01_inventory/store/db_store.rb b/01_inventory/store/db_store.rb new file mode 100644 index 0000000..1a66a6b --- /dev/null +++ b/01_inventory/store/db_store.rb @@ -0,0 +1,24 @@ +require "active_record" + +class DbArticle < ActiveRecord::Base + self.table_name = "articles" +end + +class DbStore + def create(record) + DbArticle.create(record) + end + + def update(record) + article = find_with_code(record["code"]) + article.update_attributes(record) + end + + def all_articles + DbArticle.all + end + + def find_with_code(code) + DbArticle.find_by code: code + end +end diff --git a/01_inventory/store/db_store/config.rb b/01_inventory/store/db_store/config.rb new file mode 100644 index 0000000..db95fff --- /dev/null +++ b/01_inventory/store/db_store/config.rb @@ -0,0 +1,6 @@ +require "active_record" + +ActiveRecord::Base.establish_connection({ + adapter: "sqlite3", + database: "store/db_store/inventory" +}) diff --git a/01_inventory/store/db_store/inventory b/01_inventory/store/db_store/inventory new file mode 100644 index 0000000..42703c7 Binary files /dev/null and b/01_inventory/store/db_store/inventory differ diff --git a/01_inventory/store/db_store/migrate/0_create_articles.rb b/01_inventory/store/db_store/migrate/0_create_articles.rb new file mode 100644 index 0000000..49e1e53 --- /dev/null +++ b/01_inventory/store/db_store/migrate/0_create_articles.rb @@ -0,0 +1,9 @@ +class CreateArticles < ActiveRecord::Migration + def change + create_table :articles do |t| + t.string :name + t.string :code + t.integer :quantity + end + end +end diff --git a/01_inventory/store/in_file_store.rb b/01_inventory/store/in_file_store.rb new file mode 100644 index 0000000..6109a4b --- /dev/null +++ b/01_inventory/store/in_file_store.rb @@ -0,0 +1,58 @@ +require "yaml" + +class InFileStore + def create(record) + update_records do |records| + records << record + end + end + + def update(record) + update_records do |records| + index = find_index(record) + records[index] = record + end + end + + def all_articles + load_records + end + + def find_with_code(code) + load_records.detect do |record| + record["code"] == code + end + end + + private + + attr_reader :records + + def find_index(record) + load_records.find_index do |current| + current["code"] == record["code"] + end + end + + def update_records(&block) + records = load_records + block.call(records) + save records + end + + def save(records) + File.open(file_path, "w") do |file| + file << YAML.dump(records) + end + end + + def load_records + File.open(file_path) do |file| + YAML.load(file.read) || [] + end + end + + def file_path + "./store/in_file_store/data.yml" + end +end diff --git a/01_inventory/store/in_file_store/data.yml b/01_inventory/store/in_file_store/data.yml new file mode 100644 index 0000000..941b7ff --- /dev/null +++ b/01_inventory/store/in_file_store/data.yml @@ -0,0 +1,13 @@ +--- +- name: Asd + code: asdfa + quantity: 11 +- name: uno + code: dos + quantity: 4 +- name: dos + code: tres + quantity: 1233 +- name: asdf + code: ffff + quantity: 21 diff --git a/01_inventory/store/in_memory_store.rb b/01_inventory/store/in_memory_store.rb new file mode 100644 index 0000000..53044c9 --- /dev/null +++ b/01_inventory/store/in_memory_store.rb @@ -0,0 +1,34 @@ +class InMemoryStore + def initialize(records) + @records = records + end + + def create(record) + records << record + end + + def update(record) + index = find_index(record) + records[index] = record + end + + def all_articles + records + end + + def find_with_code(code) + records.detect do |record| + record["code"] == code + end + end + + private + + attr_reader :records + + def find_index(record) + records.find_index do |current| + current["code"] == record["code"] + end + end +end diff --git a/01_inventory/views/index.erb b/01_inventory/views/index.erb new file mode 100644 index 0000000..95407bb --- /dev/null +++ b/01_inventory/views/index.erb @@ -0,0 +1,43 @@ + + + +Agregar artículo + + + + + + + + + + + <% @articles.each do |article| %> + + + + + + + <% end %> + +
NombreCodigoCantidad
<%= article.name %><%= article.code %><%= article.quantity %> +
+ +
+ +
+ +
+
diff --git a/01_inventory/views/layout.erb b/01_inventory/views/layout.erb new file mode 100644 index 0000000..7bdfa17 --- /dev/null +++ b/01_inventory/views/layout.erb @@ -0,0 +1,26 @@ + + + + + + + + Inventario + + + + + + +
+ <%= yield %> +
+ + diff --git a/01_inventory/views/new_article.erb b/01_inventory/views/new_article.erb new file mode 100644 index 0000000..d249c00 --- /dev/null +++ b/01_inventory/views/new_article.erb @@ -0,0 +1,32 @@ + + +
+
+
+
"> + + + <% if @form.name_errors %> + <%= @form.name_errors %> + <% end %> +
+
"> + + + <% if @form.code_errors %> + <%= @form.code_errors %> + <% end %> +
+
"> + + + <% if @form.quantity_errors %> + <%= @form.quantity_errors %> + <% end %> +
+ +
+
+