Sfoglia il codice sorgente

Create pokemon from parsed files via go-pkparse-server

Andrew Swistak 7 anni fa
parent
commit
056bd70fe1

+ 3 - 0
.gitignore

@@ -29,3 +29,6 @@
 
 # Ignore master key for decrypting credentials and more.
 /config/master.key
+
+# config may contain senesative info, so let's make sure not to publish that
+/config/config.yml

+ 3 - 2
Gemfile

@@ -6,6 +6,7 @@ ruby '2.6.0'
 gem 'rails', '~> 5.2.2'
 gem 'pg'
 gem 'haml'
+gem 'rest-client'
 gem 'puma', '~> 3.11'
 gem 'sass-rails', '~> 5.0'
 gem 'uglifier', '>= 1.3.0'
@@ -19,8 +20,8 @@ gem 'jbuilder', '~> 2.5'
 gem 'bootsnap', '>= 1.1.0', require: false
 
 group :development, :test do
-  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
-  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+  gem 'pry'
+  gem 'rspec-rails'
 end
 
 group :development do

+ 40 - 2
Gemfile.lock

@@ -51,7 +51,6 @@ GEM
     bootsnap (1.3.2)
       msgpack (~> 1.0)
     builder (3.2.3)
-    byebug (10.0.2)
     capybara (3.12.0)
       addressable
       mini_mime (>= 0.1.3)
@@ -65,6 +64,7 @@ GEM
     chromedriver-helper (2.1.0)
       archive-zip (~> 0.10)
       nokogiri (~> 1.8)
+    coderay (1.1.2)
     coffee-rails (4.2.2)
       coffee-script (>= 2.2.0)
       railties (>= 4.0.0)
@@ -74,6 +74,9 @@ GEM
     coffee-script-source (1.12.2)
     concurrent-ruby (1.1.4)
     crass (1.0.4)
+    diff-lcs (1.3)
+    domain_name (0.5.20180417)
+      unf (>= 0.0.5, < 1.0.0)
     erubi (1.8.0)
     execjs (2.7.0)
     ffi (1.9.25)
@@ -82,6 +85,8 @@ GEM
     haml (5.0.4)
       temple (>= 0.8.0)
       tilt
+    http-cookie (1.0.3)
+      domain_name (~> 0.5)
     i18n (1.3.0)
       concurrent-ruby (~> 1.0)
     io-like (0.3.0)
@@ -100,16 +105,23 @@ GEM
     marcel (0.3.3)
       mimemagic (~> 0.3.2)
     method_source (0.9.2)
+    mime-types (3.2.2)
+      mime-types-data (~> 3.2015)
+    mime-types-data (3.2018.0812)
     mimemagic (0.3.3)
     mini_mime (1.0.1)
     mini_portile2 (2.4.0)
     minitest (5.11.3)
     msgpack (1.2.4)
     multi_json (1.13.1)
+    netrc (0.11.0)
     nio4r (2.3.1)
     nokogiri (1.9.1)
       mini_portile2 (~> 2.4.0)
     pg (1.1.3)
+    pry (0.12.2)
+      coderay (~> 1.1.0)
+      method_source (~> 0.9.0)
     public_suffix (3.0.3)
     puma (3.12.0)
     rack (2.0.6)
@@ -144,6 +156,27 @@ GEM
     rb-inotify (0.10.0)
       ffi (~> 1.0)
     regexp_parser (1.3.0)
+    rest-client (2.0.2)
+      http-cookie (>= 1.0.2, < 2.0)
+      mime-types (>= 1.16, < 4.0)
+      netrc (~> 0.8)
+    rspec-core (3.8.0)
+      rspec-support (~> 3.8.0)
+    rspec-expectations (3.8.2)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.8.0)
+    rspec-mocks (3.8.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.8.0)
+    rspec-rails (3.8.1)
+      actionpack (>= 3.0)
+      activesupport (>= 3.0)
+      railties (>= 3.0)
+      rspec-core (~> 3.8.0)
+      rspec-expectations (~> 3.8.0)
+      rspec-mocks (~> 3.8.0)
+      rspec-support (~> 3.8.0)
+    rspec-support (3.8.0)
     ruby_dep (1.5.0)
     rubyzip (1.2.2)
     sass (3.7.2)
@@ -183,6 +216,9 @@ GEM
       thread_safe (~> 0.1)
     uglifier (4.1.20)
       execjs (>= 0.3.0, < 3)
+    unf (0.1.4)
+      unf_ext
+    unf_ext (0.0.7.5)
     web-console (3.7.0)
       actionview (>= 5.0)
       activemodel (>= 5.0)
@@ -199,7 +235,6 @@ PLATFORMS
 
 DEPENDENCIES
   bootsnap (>= 1.1.0)
-  byebug
   capybara (>= 2.15)
   chromedriver-helper
   coffee-rails (~> 4.2)
@@ -207,8 +242,11 @@ DEPENDENCIES
   jbuilder (~> 2.5)
   listen (>= 3.0.5, < 3.2)
   pg
+  pry
   puma (~> 3.11)
   rails (~> 5.2.2)
+  rest-client
+  rspec-rails
   sass-rails (~> 5.0)
   selenium-webdriver
   spring

+ 67 - 0
app/controllers/pokemon_controller.rb

@@ -0,0 +1,67 @@
+require './lib/pkparse/client'
+
+class PokemonController < ApplicationController
+  def index
+    @pokemon = Pokemon.all
+  end
+
+  def show
+    @pokemon = Pokemon.find(params[:id])
+  end
+
+  def new
+    @pokemon = Pokemon.new
+  end
+
+  def create
+    @pokemon = Pokemon.new(new_pokemon_params)
+
+    if @pokemon.save
+      redirect_to pokemon_path(@pokemon)
+    else
+      render :new
+    end
+  end
+
+  def destroy
+    pokemon = Pokemon.find(params[:id])
+    if pokemon.destroy
+      flash.now[:notice] = "Pokemon successfuly deleted."
+      redirect_to pokemon_index_path
+    else
+      flash.now[:alert] = "Failed to delete pokemon."
+      render :show
+    end
+  end
+
+  def upload
+    files = params[:pokemon]
+    response = PKParse::Client.new(files).parse
+
+    saved = false
+
+    Pokemon.transaction do
+      saved = Pokemon.create!(response.pokemon.map(&:to_h))
+    end
+
+    if saved
+      redirect_to pokemon_index_path
+    else
+      flash.now[:alert] = "Failed to commit the uploaded pokemon."
+      @pokemon = Pokemon.new
+      render :new
+    end
+  rescue PKParse::Error => e
+    flash.now[:alert] = e.message
+    @pokemon = Pokemon.new
+    render :new
+  end
+
+  private
+
+  def new_pokemon_params
+    params.require(:pokemon).permit(
+      :id, :pokedex_number, :nickname
+    )
+  end
+end

+ 4 - 0
app/views/layouts/application.html.erb

@@ -10,6 +10,10 @@
   </head>
 
   <body>
+    <% flash.each do |key, value| %>
+      <%= content_tag :div, value, class: "flash #{key}" %>
+    <% end %>
+
     <%= yield %>
   </body>
 </html>

+ 1 - 2
app/views/pokemon/index.haml

@@ -1,6 +1,5 @@
-= link_to "Create New Pokemon", new_pokemon_path
-%br
 = link_to "Upload New Pokemon", new_pokemon_path
+
 %br
 %br
 %table

+ 7 - 0
app/views/pokemon/new.haml

@@ -7,3 +7,10 @@
   = f.text_field :nickname
 
   = submit_tag 'Save'
+
+%br
+%br
+
+= form_tag({action: :upload}, multipart: true) do
+  = file_field_tag 'pokemon[]', multiple: true
+  = submit_tag 'Upload'

+ 5 - 0
config/initializers/config.rb

@@ -0,0 +1,5 @@
+YAML.load_file(Rails.root.join('config/config.yml'))[Rails.env].each do |k,v|
+  # load non-sensitive configuration from config.yml if the environment isn't
+  # already set
+  ENV[k.to_s] = v.to_s unless ENV[k.to_s]
+end

+ 4 - 0
config/initializers/pkparse.rb

@@ -0,0 +1,4 @@
+require './lib/pkparse'
+require './lib/tagged_logger'
+PKParse.service_url = ENV["PKPARSE_URL"]
+PKParse.logger = TaggedLogger.new(Rails.logger, "PKParse")

+ 2 - 0
config/routes.rb

@@ -4,4 +4,6 @@ Rails.application.routes.draw do
   root to: "pokemon#index"
 
   resources :pokemon
+  post 'pokemon/upload', to: 'pokemon#upload'
+
 end

+ 9 - 0
lib/pkparse.rb

@@ -0,0 +1,9 @@
+require './lib/pkparse/response'
+require './lib/pkparse/error'
+require './lib/pkparse/response_error'
+require './lib/pkparse/client'
+require './lib/pkparse/pokemon'
+
+module PKParse
+  mattr_accessor :service_url, :logger
+end

+ 34 - 0
lib/pkparse/client.rb

@@ -0,0 +1,34 @@
+require 'rest-client'
+
+module PKParse
+  class Client
+    attr_accessor :files
+
+    def initialize(files)
+      @files = files
+    end
+
+    def parse
+      response = RestClient.post("#{PKParse.service_url}/parse", form_key => files, multipart: true)
+      response = JSON.parse(response, symbolize_keys: true)
+
+      PKParse::Response.new(response)
+    rescue RestClient::Exception => e
+      raise PKParse::ResponseError.new(e)
+    rescue JSON::ParserError => e
+      raise PKParse::Error.new(e, "Received invalid parse response")
+    end
+
+    private
+
+    def form_key
+      # RestClient will add brackets to array form values, but go-pkparse-server
+      # expects brackets regardless of multivalue or not.
+      if files.size > 1
+        "pkmn"
+      else
+        "pkmn[]"
+      end
+    end
+  end
+end

+ 23 - 0
lib/pkparse/error.rb

@@ -0,0 +1,23 @@
+module PKParse
+  class Error < StandardError
+    attr_accessor :original_exception
+    attr_writer :message
+
+    def initialize(e, message = nil)
+      @original_exception = e
+      @message = message
+    end
+
+    def to_s
+      message
+    end
+
+    def message
+      @message || default_message
+    end
+
+    def default_message
+      'An error occurred while attempting to parse one or more pokemon.'
+    end
+  end
+end

+ 26 - 0
lib/pkparse/pokemon.rb

@@ -0,0 +1,26 @@
+module PKParse
+  # Define a PKParse::Pokemon to keep Rails' model distinct and allow this to be
+  # updated independently without fear of breaking the app.
+  class Pokemon
+    attr_accessor :pokedex_number, :nickname, :raw_nickname, :raw_pokemon
+
+    def initialize(attributes={})
+      attributes.each do |attr,v|
+        send("#{attr}=", v) rescue nil
+      end
+    end
+
+    def to_h
+      {
+        pokedex_number: pokedex_number,
+        nickname: nickname,
+        raw_nickname: raw_nickname,
+        raw_pokemon: raw_pokemon,
+      }
+    end
+
+    def to_json
+      to_h
+    end
+  end
+end

+ 18 - 0
lib/pkparse/response.rb

@@ -0,0 +1,18 @@
+module PKParse
+  class Response
+    attr_accessor :response, :pokemon
+
+    def initialize(response)
+      @response = response
+      build_pokemon
+    end
+
+    private
+
+    def build_pokemon
+      @pokemon = response.map do |pkmn|
+        PKParse::Pokemon.new(pkmn)
+      end
+    end
+  end
+end

+ 18 - 0
lib/pkparse/response_error.rb

@@ -0,0 +1,18 @@
+module PKParse
+  class ResponseError < Error
+    def initialize(e)
+      super(e)
+      parse_body
+    end
+
+    private
+
+    def parse_body
+      body = original_exception.http_body
+      parsed_body = JSON.parse(body, symbolize_keys: true)
+      @message = parsed_body[:error]
+    rescue
+      PKParse.logger.error("Exception parsing ResponseError http body:\n#{$!}\n#{$!.backtrace.join("\n")}")
+    end
+  end
+end

+ 30 - 0
lib/tagged_logger.rb

@@ -0,0 +1,30 @@
+class TaggedLogger
+  attr_reader :logger, :tag
+
+  def initialize(logger, tag)
+    @logger = logger
+    @tag = tag
+  end
+
+  delegate :tagged, to: :logger
+
+  def debug(msg)
+    logger.tagged(@tag) { logger.debug(msg) }
+  end
+
+  def info(msg)
+    logger.tagged(@tag) { logger.info(msg) }
+  end
+
+  def warn(msg)
+    logger.tagged(@tag) { logger.warn(msg) }
+  end
+
+  def error(msg)
+    logger.tagged(@tag) { logger.error(msg) }
+  end
+
+  def fatal(msg)
+    logger.tagged(@tag) { logger.fatal(msg) }
+  end
+end