├── log └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── .prettierrc ├── .ruby-version ├── spec ├── services │ └── .keep ├── support │ ├── .rubocop.yml │ └── request_spec_helper.rb ├── .rubocop.yml ├── controllers │ └── application_controller_spec.rb ├── factories │ └── breweries.rb ├── models │ └── brewery_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── requests │ ├── breweries_spec.rb │ └── v1_breweries_spec.rb ├── app ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ └── brewery.rb ├── views │ └── layouts │ │ ├── mailer.text.erb │ │ └── mailer.html.erb ├── services │ ├── message.rb │ ├── update_geocodes.rb │ ├── update_state_abbreviations.rb │ └── import │ │ └── breweries.rb ├── controllers │ ├── breweries │ │ └── breweries_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ ├── response.rb │ │ └── exception_handler.rb │ └── v1 │ │ └── breweries │ │ └── breweries_controller.rb └── serializers │ └── brewery_serializer.rb ├── .ruby-gemset ├── public ├── robots.txt └── favicon.ico ├── .rspec ├── obdb-logo-md.jpg ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config ├── spring.rb ├── environment.rb ├── initializers │ ├── kaminari_config.rb │ ├── mime_types.rb │ ├── sentry.rb │ ├── filter_parameter_logging.rb │ ├── constants.rb │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── cors.rb │ └── inflections.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── routes.rb ├── locales │ └── en.yml ├── storage.yml ├── puma.rb ├── application.rb └── environments │ ├── test.rb │ ├── development.rb │ ├── production.rb │ └── staging.rb ├── config.ru ├── Rakefile ├── db ├── seeds.rb └── schema.rb ├── lib ├── uuid_validator.rb ├── tasks │ └── breweries │ │ └── breweries_tasks.rake └── import │ └── brewers_association │ ├── mississippi.html │ ├── north_dakota.html │ ├── district_of_columbia.html │ └── hawaii.html ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ └── main.yml └── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── Guardfile ├── LICENSE ├── .gitignore ├── Gemfile ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /spec/services/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | openbrewery 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation -------------------------------------------------------------------------------- /spec/support/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ../../.rubocop.yml 3 | -------------------------------------------------------------------------------- /obdb-logo-md.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openbrewerydb/openbrewerydb-rails-api/HEAD/obdb-logo-md.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openbrewerydb/openbrewerydb-rails-api/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w[ 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ].each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /spec/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - ../.rubocop.yml 3 | 4 | Style/StringLiterals: 5 | EnforcedStyle: double_quotes 6 | Exclude: 7 | - './*_helper.rb' 8 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/support/request_spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RequestSpecHelper 4 | def json 5 | JSON.parse(response.body) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Kaminari.configure do |config| 4 | config.default_per_page = 50 5 | config.max_per_page = 200 6 | end 7 | -------------------------------------------------------------------------------- /app/services/message.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Message class 4 | class Message 5 | def self.account_created 6 | 'Account created successfully!' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/controllers/application_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe ApplicationController, type: :controller do 6 | end 7 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # ApplicationRecord model class 4 | class ApplicationRecord < ActiveRecord::Base 5 | self.abstract_class = true 6 | end 7 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/controllers/breweries/breweries_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Breweries 4 | # BreweryController class 5 | class BreweriesController < V1::Breweries::BreweriesController 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # ApplicationController class 4 | class ApplicationController < ActionController::API 5 | include Response 6 | include ExceptionHandler 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/concerns/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Response helper module 4 | module Response 5 | def json_response(object, status = :ok) 6 | render json: object, status: status 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sentry.init do |config| 4 | config.dsn = ENV['SENTRY_DSN'] 5 | config.rails.report_rescued_exceptions = true 6 | config.breadcrumbs_logger = [:active_support_logger] 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: openbrewerydb-rest-api_production 11 | -------------------------------------------------------------------------------- /config/initializers/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | BREWERY_TYPES = %w[ 4 | micro 5 | nano 6 | regional 7 | brewpub 8 | large 9 | planning 10 | bar 11 | contract 12 | proprietor 13 | closed 14 | ].freeze 15 | 16 | MAX_PER_PAGE = 50 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /lib/uuid_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # UUID Validator for Models 4 | class UuidValidator < ActiveModel::EachValidator 5 | def validate_each(record, attribute, value) 6 | return if value =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i 7 | 8 | msg = options[:message] || 'is not a valid UUID' 9 | record.errors.add(attribute, msg) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | /jyO6pad7hqifL8IAq3RPZvwiHWXQ/K8qwJ9T9UWPIGdrujThLHnh1JWwYd8FigeTil3nDYhnwwEUEk8hxcjTAu/9xfRT/bkc+Ki/c54vIcRSknMfaxsjp5xnH82Jn6vOpz+rIK2KZ8+YQ6Dv+siqffzkx9bjuhngwNLMv1Qi93cQienUwsIA27WCbH2ngpnkM4EHlH7QAKeQ6mUb4XAjBYMdOaHxqVWzzbNsbbDi2hZemhh+WVTpKW5oL3nV580Te8rYAMtIAYebSbZi9FD2h1g6ToKEtZS2EoAup6HGVZBuu3/um5XG1apPObEsEEti4uqzzZEphB4oXZPfxdVHY++CmDmoEiuluB4z/6AwKVCVBYD3UBocI2tHXHAMsSCJLeEI0LlzVIYKaRvBOg0SODiQk0wZZwqMoI4--cUkcFed8KWIMIDaC--f/phs+koBfulzTFW7q4vfA== -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /lib/tasks/breweries/breweries_tasks.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :breweries do 4 | namespace :import do 5 | desc 'Import OpenBreweryDb data' 6 | task breweries: :environment do 7 | Import::Breweries.perform 8 | end 9 | end 10 | 11 | desc 'Convert any state abbreviations into full state (one-time task)' 12 | task update_state_abbreviations: :environment do 13 | UpdateStateAbbreviations.perform 14 | end 15 | 16 | desc 'Process Geocodes for all Breweries' 17 | task update_geocodes: :environment do 18 | UpdateGeocodes.perform 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 5 | host: <%= ENV.fetch("POSTGRES_HOST") { "localhost" } %> 6 | user: <%= ENV.fetch("POSTGRES_USER") { "" } %> 7 | password: <%= ENV.fetch("POSTGRES_PASSWORD") { "" } %> 8 | timeout: 5000 9 | 10 | development: 11 | <<: *default 12 | database: openbrewerydb_development 13 | 14 | test: 15 | <<: *default 16 | database: openbrewerydb_test 17 | 18 | staging: 19 | url: <%= ENV['DATABASE_URL'] %> 20 | 21 | production: 22 | url: <%= ENV['DATABASE_URL'] %> 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | # Bug Report 7 | 8 | ## Describe the bug 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | ## Steps to reproduce bug 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | ### Expected behavior 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | ## Additional context 28 | 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /app/serializers/brewery_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # JSON Serializer for a Brewery model 4 | class BrewerySerializer < ActiveModel::Serializer 5 | attribute :id 6 | attribute :name 7 | attribute :brewery_type 8 | attribute :address_1 9 | attribute :address_2 10 | attribute :address_3 11 | attribute :city 12 | attribute :state_province 13 | attribute :postal_code 14 | attribute :country 15 | attribute :longitude 16 | attribute :latitude 17 | attribute :phone 18 | attribute :website_url 19 | 20 | # DEPRECATED - Will be removed at a TBD date 21 | attribute :state 22 | attribute :street 23 | end 24 | -------------------------------------------------------------------------------- /spec/factories/breweries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :brewery do 5 | id { Faker::Internet.uuid } 6 | name { "#{Faker::Company.name} Brewery" } 7 | brewery_type { %w[micro planning brewpub].sample } 8 | address_1 { Faker::Address.street_address } 9 | address_2 { Faker::Address.secondary_address } 10 | address_3 { nil } 11 | city { Faker::Address.city } 12 | state_province { Faker::Address.state } 13 | phone { Faker::PhoneNumber.phone_number } 14 | postal_code { Faker::Address.postcode } 15 | country { Faker::Address.country } 16 | website_url { Faker::Internet.url } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | # Feature request 7 | 8 | ## Overview 9 | 10 | A clear and concise description of what the feature or problem addresses. 11 | 12 | Include: 13 | 14 | - Solution you want 15 | - Alternatives you have considered 16 | 17 | ## Tasks 18 | 19 | - [ ] Task 1 20 | - [ ] Task 2 21 | - [ ] Task 3 22 | 23 | ## Screenshots & Diagrams 24 | 25 | Add any screenshots or diagrams here to help with implementation. 26 | 27 | ## Additional context 28 | 29 | Add any other context. 30 | 31 | ## Implementation notes 32 | 33 | This section will be used by the assigned developer for any additional notes. 34 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | puts "\n== Updating database ==" 21 | system! 'bin/rails db:migrate' 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system! 'bin/rails log:clear tmp:clear' 25 | 26 | puts "\n== Restarting application server ==" 27 | system! 'bin/rails restart' 28 | end 29 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | guard :rspec, cmd: 'bundle exec rspec' do 4 | watch('spec/spec_helper.rb') { 'spec' } 5 | watch('spec/rails_helper.rb') { 'spec' } 6 | watch('config/routes.rb') { 'spec/routing' } 7 | watch('app/controllers/application_controller.rb') { 'spec/controllers' } 8 | watch(%r{^spec/.+_spec\.rb$}) 9 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 10 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 11 | watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) do |m| 12 | "spec/#{m[1]}#{m[2]}_spec.rb" 13 | end 14 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) do |m| 15 | [ 16 | "spec/routing/#{m[1]}_routing_spec.rb", 17 | "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", 18 | "spec/acceptance/#{m[1]}_spec.rb", 19 | "spec/requests/#{m[1]}_spec.rb" 20 | ] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | namespace :breweries do 5 | get '/', to: 'breweries#index' 6 | get '/autocomplete', to: 'breweries#autocomplete' 7 | get '/meta', to: 'breweries#meta' 8 | get '/random', to: 'breweries#random' 9 | get '/search', to: 'breweries#search' 10 | get '/:id', to: 'breweries#show' 11 | end 12 | 13 | namespace :v1 do 14 | namespace :breweries do 15 | get '/', to: 'breweries#index' 16 | get '/autocomplete', to: 'breweries#autocomplete' 17 | get '/meta', to: 'breweries#meta' 18 | get '/random', to: 'breweries#random' 19 | get '/search', to: 'breweries#search' 20 | get '/:id', to: 'breweries#show' 21 | end 22 | end 23 | 24 | # Otherwise, redirect to www 25 | root to: redirect('https://www.openbrewerydb.org/') 26 | end 27 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:setup' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chris Mears 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/models/brewery_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe Brewery do 6 | describe "#model_validation" do 7 | subject(:brewery) { described_class.new(id:, name:, city:, state_province:, country:) } 8 | 9 | let(:id) { SecureRandom.uuid } 10 | let(:name) { "brewery-name" } 11 | let(:city) { "brewery-city" } 12 | let(:state_province) { nil } 13 | let(:country) { nil } 14 | 15 | context "when state is present" do 16 | let(:state_province) { "brewery-state" } 17 | let(:country) { "United States" } 18 | 19 | it "validates successfully" do 20 | expect(brewery.valid?).to be true 21 | end 22 | end 23 | 24 | context "when state is nil" do 25 | let(:country) { "United States" } 26 | 27 | it "fails validation" do 28 | expect(brewery.valid?).to be false 29 | end 30 | end 31 | end 32 | 33 | describe "#address" do 34 | subject(:brewery) { create(:brewery) } 35 | 36 | it "returns a full address" do 37 | expect(brewery.address).to eq( 38 | "#{brewery.address_1}, #{brewery.city}, #{brewery.state_province}, #{brewery.country}" 39 | ) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test Suite 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | container: 10 | image: ruby:3.2.0 11 | 12 | services: 13 | postgres: 14 | image: postgres:14.2 15 | env: 16 | POSTGRES_USER: postgres 17 | POSTGRES_PASSWORD: postgres 18 | POSTGRES_DB: postgres 19 | ports: 20 | - 5432:5432 21 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 22 | 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: Build 26 | env: 27 | POSTGRES_HOST: postgres 28 | POSTGRES_USER: postgres 29 | POSTGRES_PASSWORD: postgres 30 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} 31 | RAILS_ENV: test 32 | run: | 33 | gem install bundler 34 | bundle install --jobs 4 --retry 3 35 | bundle exec rails db:setup 36 | - name: Run Tests 37 | env: 38 | POSTGRES_HOST: postgres 39 | POSTGRES_USER: postgres 40 | POSTGRES_PASSWORD: postgres 41 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} 42 | RAILS_ENV: test 43 | run: bundle exec rake 44 | -------------------------------------------------------------------------------- /app/controllers/concerns/exception_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Exception Handler module 4 | module ExceptionHandler 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity 9 | rescue_from ActiveRecord::RecordNotFound, with: :not_found 10 | rescue_from Faraday::ConnectionFailed, with: :service_unavailable 11 | rescue_from Elasticsearch::Transport::Transport::ServerError, with: :too_many_requests 12 | end 13 | 14 | private 15 | 16 | def not_found(exception) 17 | json_response({ message: exception.message }, :not_found) 18 | end 19 | 20 | def service_unavailable 21 | json_response( 22 | { 23 | message: 'There is an issue connecting to the ElasticSearch server. ' \ 24 | 'Please try again or use other filter options. Example: ' \ 25 | 'https://api.openbrewerydb.org/breweries?by_state=OH&sort=city' 26 | }, 27 | :service_unavailable 28 | ) 29 | end 30 | 31 | def too_many_requests 32 | json_response( 33 | { 34 | message: 'Concurrent request limit exceeded. Please delay concurrent calls using debounce or throttle.' 35 | }, 36 | :too_many_requests 37 | ) 38 | end 39 | 40 | def unprocessable_entity(exception) 41 | json_response({ message: exception.message }, :unprocessable_entity) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull request checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) 6 | - [ ] Tests pass (`bundle exec rake`) 7 | - [ ] CHANGELOG.md updated 8 | 9 | ## Pull request type 10 | 11 | 12 | 13 | 14 | Please check the type of change your PR introduces: 15 | 16 | - [ ] Bugfix 17 | - [ ] Feature 18 | - [ ] Code style update (formatting, renaming) 19 | - [ ] Refactoring (no functional changes, no api changes) 20 | - [ ] Build related changes 21 | - [ ] Documentation content changes 22 | - [ ] Other (please describe): 23 | 24 | ## Current behavior 25 | 26 | 27 | 28 | Issue Number: N/A 29 | 30 | ## New behavior 31 | 32 | 33 | 34 | ## Does this introduce a breaking change 35 | 36 | - [ ] Yes 37 | - [ ] No 38 | 39 | 40 | 41 | ## Other information 42 | 43 | 44 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. 30 | # 31 | # preload_app! 32 | 33 | # Allow puma to be restarted by `rails restart` command. 34 | plugin :tmp_restart 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore uploaded files in development 11 | /storage/* 12 | 13 | *.rbc 14 | capybara-*.html 15 | .rspec 16 | /log 17 | /tmp 18 | /db/*.sqlite3 19 | /db/*.sqlite3-journal 20 | /public/system 21 | /coverage/ 22 | /spec/tmp 23 | /spec/examples.txt 24 | *.orig 25 | rerun.txt 26 | pickle-email-*.html 27 | 28 | /config/initializers/secret_token.rb 29 | /config/master.key 30 | /config/secrets.yml 31 | 32 | # dotenv 33 | # TODO Comment out this rule if environment variables can be committed 34 | .env* 35 | 36 | ## Environment normalization: 37 | /.bundle 38 | /vendor/bundle 39 | 40 | # these should all be checked in to normalize the environment: 41 | # Gemfile.lock, .ruby-version, .ruby-gemset 42 | 43 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 44 | .rvmrc 45 | 46 | # if using bower-rails ignore default bower_components path bower.json files 47 | /vendor/assets/bower_components 48 | *.bowerrc 49 | bower.json 50 | 51 | # Ignore pow environment settings 52 | .powenv 53 | 54 | # Ignore Byebug command history file. 55 | .byebug_history 56 | 57 | # Ignore node_modules 58 | node_modules/ 59 | 60 | # Ignore vscode directories 61 | .vscode/ 62 | 63 | .DS_Store 64 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2023_03_21_000000) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "fuzzystrmatch" 17 | enable_extension "pg_trgm" 18 | enable_extension "pgcrypto" 19 | enable_extension "plpgsql" 20 | 21 | create_table "breweries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| 22 | t.string "name", null: false 23 | t.string "brewery_type", null: false 24 | t.string "address_1" 25 | t.string "address_2" 26 | t.string "address_3" 27 | t.string "city", null: false 28 | t.string "state_province", null: false 29 | t.string "country", null: false 30 | t.string "postal_code", null: false 31 | t.string "website_url" 32 | t.string "phone" 33 | t.decimal "longitude" 34 | t.decimal "latitude" 35 | t.index ["id"], name: "index_breweries_on_id" 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | ruby '3.2.0' 7 | 8 | # Backend 9 | gem 'bcrypt', '~> 3.1' 10 | gem 'faraday', '~> 1.4' 11 | gem 'geocoder', '~> 1.6' 12 | gem 'geokit-rails', '~> 2.3' 13 | gem 'has_scope', '~> 0.8' 14 | gem 'nokogiri', '~> 1.15' 15 | gem 'puma', '~> 5.6' 16 | gem 'rack', '>= 2.2.6' 17 | gem 'rack-cors', '~> 1.1' 18 | gem 'rails', '~> 6.1' 19 | gem 'rexml' 20 | gem 'sentry-rails' 21 | 22 | # Elastic Search 23 | gem 'searchkick', '~> 4.6' 24 | 25 | # Pagination 26 | gem 'kaminari', '~> 1.2' 27 | 28 | # Database 29 | gem 'pg', '~> 1.4' 30 | 31 | # Frontend 32 | gem 'active_model_serializers', '~> 0.10' 33 | gem 'bootsnap', '>= 1.1', require: false 34 | gem 'colorize', '~> 0.8' 35 | 36 | group :development, :test do 37 | gem 'byebug', platforms: %i[mri mingw x64_mingw] 38 | gem 'dotenv-rails', '~> 2.7' 39 | gem 'guard-rspec', '~> 4.7' 40 | gem 'rspec-rails', '~> 5.0' 41 | gem 'rubocop-rails', '~> 2.10', require: false 42 | gem 'rubocop-rspec', '~> 2.3', require: false 43 | end 44 | 45 | group :test do 46 | gem 'database_cleaner', '~> 2.0' 47 | gem 'factory_bot_rails', '~> 6.2' 48 | gem 'faker', '~> 2.17' 49 | gem 'rspec_junit_formatter', '~> 0.4' 50 | gem 'shoulda-matchers', '~> 3.1' 51 | gem 'simplecov', '~> 0.21', require: false 52 | end 53 | 54 | group :development do 55 | gem 'listen', '~> 3.2' 56 | gem 'spring', '~> 2.1' 57 | gem 'spring-watcher-listen', '~> 2.0' 58 | end 59 | 60 | group :production do 61 | gem 'cloudflare-rails', '~> 2.0' 62 | end 63 | 64 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 65 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 66 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails' 6 | require 'active_model/railtie' 7 | require 'active_job/railtie' 8 | require 'active_record/railtie' 9 | require 'active_storage/engine' 10 | require 'action_controller/railtie' 11 | # require 'action_mailer/railtie' 12 | require 'action_view/railtie' 13 | require 'action_cable/engine' 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module OpenbrewerydbRestApi 20 | class Application < Rails::Application 21 | # Initialize configuration defaults for originally generated Rails version. 22 | config.load_defaults 5.2 23 | 24 | # Settings in config/environments/* take precedence over those specified here. 25 | # Application configuration can go into files in config/initializers 26 | # -- all .rb files in that directory are automatically loaded after loading 27 | # the framework and any gems in your application. 28 | 29 | # Only loads a smaller set of middleware suitable for API only apps. 30 | # Middleware like session, flash, cookies can be added back manually. 31 | # Skip views, helpers and assets when generating a new resource. 32 | config.api_only = true 33 | 34 | # Since this is a public API, we don't really care about IP spoofing. 35 | config.action_dispatch.ip_spoofing_check = false 36 | 37 | # Cross Origin Resource Sharing (CORS) settings 38 | # Allow GET requests from any origin on any resource 39 | config.middleware.insert_before 0, Rack::Cors do 40 | allow do 41 | origins '*' 42 | resource '*', headers: :any, methods: %i[get] 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | # config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 35 | # config.action_mailer.perform_caching = false 36 | 37 | # Tell Action Mailer not to deliver emails to the real world. 38 | # The :test delivery method accumulates sent emails in the 39 | # ActionMailer::Base.deliveries array. 40 | # config.action_mailer.delivery_method = :test 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations 46 | # config.action_view.raise_on_missing_translations = true 47 | end 48 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | 22 | config.cache_store = :memory_store 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 25 | } 26 | else 27 | config.action_controller.perform_caching = false 28 | 29 | config.cache_store = :null_store 30 | end 31 | 32 | # Store uploaded files on the local file system (see config/storage.yml for options) 33 | config.active_storage.service = :local 34 | 35 | # config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 36 | # Don't care if the mailer can't send. 37 | # config.action_mailer.raise_delivery_errors = false 38 | # config.action_mailer.perform_caching = false 39 | 40 | # Print deprecation notices to the Rails logger. 41 | config.active_support.deprecation = :log 42 | 43 | # Raise an error on page load if there are pending migrations. 44 | config.active_record.migration_error = :page_load 45 | 46 | # Highlight code that triggered database queries in logs. 47 | config.active_record.verbose_query_logs = true 48 | 49 | # Raises error for missing translations 50 | # config.action_view.raise_on_missing_translations = true 51 | 52 | # Use an evented file watcher to asynchronously detect changes in source code, 53 | # routes, locales, etc. This feature depends on the listen gem. 54 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 55 | 56 | Rails.application.routes.default_url_options = { host: 'localhost' } 57 | end 58 | -------------------------------------------------------------------------------- /app/services/update_geocodes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Task to update the geocoding on breweries in DB 4 | class UpdateGeocodes 5 | def initialize 6 | @log = ActiveSupport::Logger.new('log/update_geocodes.log') 7 | @dry_run = ENV['DRY_RUN'].present? ? ENV['DRY_RUN'].casecmp('true').zero? : false 8 | @counter = { updated: 0, skipped: 0, failed: 0, total: 0 } 9 | @offset = ENV['OFFSET'].present? ? ENV['OFFSET'] : nil 10 | end 11 | 12 | def self.perform 13 | new.perform 14 | end 15 | 16 | def perform 17 | start_time = Time.now 18 | @log.info "Task started at #{start_time}" 19 | 20 | puts "\n!!!!! DRY RUN !!!!!\nNO DATA WILL BE CHANGED\n".yellow if @dry_run 21 | 22 | process_brewery_batches 23 | 24 | output_summary 25 | 26 | end_time = Time.now 27 | duration = (start_time - end_time).round(2).abs 28 | log_and_print("\nTask finished at #{end_time} and lasted #{duration}s.") 29 | @log.close 30 | end 31 | 32 | private 33 | 34 | def log_and_print(message) 35 | puts(message) 36 | @log.info(message.uncolorize) 37 | end 38 | 39 | def output_summary 40 | log_and_print("\n---------------\nTotal: #{@counter[:total]}".white) 41 | log_and_print("Updated: #{@counter[:updated]}".green) 42 | log_and_print("Skipped: #{@counter[:skipped]}".blue) 43 | log_and_print("Failed: #{@counter[:failed]}".red) 44 | log_and_print('----------------'.white) 45 | end 46 | 47 | def process_brewery_batches 48 | Brewery.find_in_batches(start: @offset) do |group| 49 | process_breweries(group) 50 | end 51 | end 52 | 53 | def process_breweries(breweries = []) 54 | breweries.each do |brewery| 55 | @counter[:total] += 1 56 | 57 | puts "#{brewery.id}. #{brewery.name} - #{brewery.address}".blue 58 | 59 | if brewery.address_1.present? && 60 | brewery.latitude.blank? && 61 | brewery.address_1.match?(/[Ste]/) 62 | 63 | unless @dry_run 64 | if brewery.save 65 | puts " #{brewery.latitude}, #{brewery.longitude}".green 66 | @counter[:updated] += 1 67 | sleep 1 68 | else 69 | puts " #{brewery.name} failed to update!".red 70 | @counter[:failed] += 1 71 | end 72 | end 73 | else 74 | @counter[:skipped] += 1 75 | puts ' Skipped!'.red 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/models/brewery.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'uuid_validator' 4 | 5 | # Brewery Model 6 | class Brewery < ApplicationRecord 7 | self.table_name = ENV.fetch('BREWERY_TABLE', 'breweries') 8 | 9 | # Because the table is usually a view (views don't specify a primary key) 10 | self.primary_key = 'id' 11 | 12 | # Elastic Search via Searchkick 13 | searchkick 14 | 15 | geocoded_by :address 16 | 17 | acts_as_mappable lat_column_name: :latitude, lng_column_name: :longitude 18 | 19 | validates :id, presence: true, uniqueness: true, uuid: true 20 | validates :name, presence: true 21 | validates :city, presence: true 22 | validates :state_province, presence: true 23 | validates :country, presence: true 24 | 25 | # Filter by `brewery_type` 26 | scope :by_type, ->(type) { where('lower(brewery_type) = ?', type.downcase) } 27 | # Filter by multiple, comma-separated `id`s 28 | scope :by_ids, ->(ids) { where(id: ids.split(',').first(50)) } 29 | # Filter by `city` 30 | scope :by_city, lambda { |city| 31 | where( 32 | 'lower(city) LIKE ?', 33 | "%#{sanitize_sql_like(city.gsub('+', ' ').downcase)}%" 34 | ) 35 | } 36 | # Filter by `country` 37 | scope :by_country, lambda { |country| 38 | where( 39 | 'lower(country) LIKE ?', 40 | "%#{sanitize_sql_like(country.gsub('+', ' ').downcase)}%" 41 | ) 42 | } 43 | # Sort by the distance from a `latitude`,`longitude` geo point 44 | scope :by_dist, lambda { |coords| 45 | by_distance(origin: coords.split(',').map(&:to_f).first(2)) 46 | } 47 | # Filter by `name` 48 | scope :by_name, lambda { |name| 49 | where( 50 | 'lower(name) LIKE ?', 51 | "%#{sanitize_sql_like(name.gsub('+', ' ').downcase)}%" 52 | ) 53 | } 54 | # Filter by `postal_code` 55 | scope :by_postal, lambda { |postal| 56 | where('postal_code LIKE ?', "%#{sanitize_sql_like(postal)}%") 57 | } 58 | # Filter by `state_province` 59 | scope :by_state, lambda { |state| 60 | where( 61 | 'lower(state_province) LIKE ?', 62 | "%#{sanitize_sql_like(state.gsub('+', ' ').downcase)}%" 63 | ) 64 | } 65 | # Filter by exluding the comma-separated `brewery_types` 66 | scope :exclude_types, lambda { |types| 67 | where('lower(brewery_type) NOT IN (?)', types.split(',')) 68 | } 69 | 70 | def address 71 | [address_1, city, state_province, country].join(', ') 72 | end 73 | 74 | # DEPRECATED - Will be removed at a TBD date 75 | def state 76 | state_province 77 | end 78 | 79 | # DEPRECATED - Will be removed at a TBD date 80 | def street 81 | address_1 82 | end 83 | 84 | # For Searchkick 85 | def search_data 86 | { 87 | id:, 88 | name:, 89 | city:, 90 | state_province:, 91 | postal_code:, 92 | country:, 93 | latitude:, 94 | longitude: 95 | } 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | 2 | require: rubocop-rspec 3 | 4 | AllCops: 5 | NewCops: enable 6 | TargetRubyVersion: 3.1 7 | Include: 8 | - "app/**/*" 9 | - "config.ru" 10 | - "Gemfile" 11 | - "Guardfile" 12 | - "lib/**/*" 13 | - "Rakefile" 14 | - 'spec**/*.rb' 15 | 16 | Exclude: 17 | - "app/views/**/*" 18 | - "bin/*" 19 | - "db/schema.rb" 20 | - "db/migrate/*" 21 | - "log/**/*" 22 | - "node_modules/**/*" 23 | - "public/**/*" 24 | - "scripts/**/*" 25 | - "vendor/**/*" 26 | - "tmp/**/*" 27 | - ".git/**/*" 28 | - "lib/import/**/*" 29 | 30 | Naming/FileName: 31 | Exclude: 32 | - "Gemfile" 33 | - "Guardfile" 34 | - "Rakefile" 35 | Layout/HashAlignment: 36 | EnforcedHashRocketStyle: table 37 | Layout/LineLength: 38 | Max: 120 39 | Layout/SpaceBeforeBrackets: 40 | Enabled: true 41 | Lint/AmbiguousAssignment: 42 | Enabled: true 43 | Lint/DeprecatedConstants: 44 | Enabled: true 45 | Lint/DuplicateBranch: 46 | Enabled: true 47 | Lint/DuplicateRegexpCharacterClassElement: 48 | Enabled: true 49 | Lint/EmptyBlock: 50 | Enabled: true 51 | Lint/EmptyClass: 52 | Enabled: true 53 | Lint/EmptyFile: 54 | Enabled: false 55 | Lint/LambdaWithoutLiteralBlock: 56 | Enabled: true 57 | Lint/NoReturnInBeginEndBlocks: 58 | Enabled: true 59 | Lint/NumberedParameterAssignment: 60 | Enabled: true 61 | Lint/OrAssignmentToConstant: 62 | Enabled: true 63 | Lint/RedundantDirGlobSort: 64 | Enabled: true 65 | Lint/SymbolConversion: 66 | Enabled: true 67 | Lint/ToEnumArguments: 68 | Enabled: true 69 | Lint/TripleQuotes: 70 | Enabled: true 71 | Lint/UnexpectedBlockArity: 72 | Enabled: true 73 | Lint/UnmodifiedReduceAccumulator: 74 | Enabled: true 75 | Metrics/AbcSize: 76 | Enabled: false 77 | Metrics/BlockLength: 78 | Enabled: false 79 | Exclude: 80 | - "Guardfile" 81 | Metrics/ClassLength: 82 | Enabled: false 83 | Metrics/CyclomaticComplexity: 84 | Enabled: false 85 | Metrics/MethodLength: 86 | Enabled: false 87 | Metrics/PerceivedComplexity: 88 | Enabled: false 89 | Naming/VariableNumber: 90 | EnforcedStyle: snake_case 91 | RSpec/HookArgument: 92 | Enabled: false 93 | Style/ArgumentsForwarding: 94 | Enabled: true 95 | Style/CollectionCompact: 96 | Enabled: true 97 | Style/Documentation: 98 | Enabled: true 99 | Style/DocumentDynamicEvalDefinition: 100 | Enabled: true 101 | Style/EndlessMethod: 102 | Enabled: true 103 | Style/HashConversion: 104 | Enabled: true 105 | Style/HashExcept: 106 | Enabled: true 107 | Style/IfWithBooleanLiteralBranches: 108 | Enabled: true 109 | Style/NegatedIfElseCondition: 110 | Enabled: true 111 | Style/NilLambda: 112 | Enabled: true 113 | Style/RedundantArgument: 114 | Enabled: true 115 | Style/StringChars: 116 | Enabled: true 117 | Style/SwapValues: 118 | Enabled: true 119 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | SimpleCov.start 5 | 6 | # This file is copied to spec/ when you run 'rails generate rspec:install' 7 | require 'spec_helper' 8 | 9 | ENV['RAILS_ENV'] ||= 'test' 10 | require File.expand_path('../config/environment', __dir__) 11 | # Prevent database truncation if the environment is production 12 | abort('The Rails environment is running in production mode!') if Rails.env.production? 13 | require 'rspec/rails' 14 | 15 | # Add additional requires below this line. Rails is not loaded until this point! 16 | require 'database_cleaner' 17 | 18 | # Requires supporting ruby files with custom matchers and macros, etc, in 19 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 20 | # run as spec files by default. This means that files in spec/support that end 21 | # in _spec.rb will both be required and run as specs, causing the specs to be 22 | # run twice. It is recommended that you do not name files matching this glob to 23 | # end with _spec.rb. You can configure this pattern with the --pattern 24 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 25 | # 26 | # The following line is provided for convenience purposes. It has the downside 27 | # of increasing the boot-up time by auto-requiring all files in the support 28 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 29 | # require only the support files necessary. 30 | # 31 | Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f } 32 | 33 | # Checks for pending migrations and applies them before tests are run. 34 | # If you are not using ActiveRecord, you can remove this line. 35 | ActiveRecord::Migration.maintain_test_schema! 36 | 37 | RSpec.configure do |config| 38 | config.include FactoryBot::Syntax::Methods 39 | config.include RequestSpecHelper 40 | 41 | config.before(:suite) do 42 | DatabaseCleaner.clean_with(:truncation) 43 | DatabaseCleaner.strategy = :transaction 44 | 45 | # Disable Elastic Search testing for now (too slow even with test clusters) 46 | Searchkick.disable_callbacks 47 | end 48 | 49 | config.around(:each) do |example| 50 | DatabaseCleaner.cleaning do 51 | example.run 52 | end 53 | end 54 | 55 | # For when we want to test search 56 | config.around(:each, search: true) do |example| 57 | Searchkick.callbacks(true) do 58 | example.run 59 | end 60 | end 61 | 62 | config.use_transactional_fixtures = true 63 | config.infer_spec_type_from_file_location! 64 | 65 | # Filter lines from Rails gems in backtraces. 66 | config.filter_rails_from_backtrace! 67 | # arbitrary gems may also be filtered via: 68 | # config.filter_gems_from_backtrace("gem name") 69 | end 70 | 71 | Shoulda::Matchers.configure do |config| 72 | config.integrate do |with| 73 | with.test_framework :rspec 74 | with.library :rails 75 | end 76 | end 77 | 78 | # Disable Geocoder API calls 79 | Geocoder.configure(lookup: :test) 80 | Geocoder::Lookup::Test.set_default_stub( 81 | [ 82 | { 83 | 'coordinates' => [40.7143528, -74.0059731], 84 | 'address' => 'New York, NY, USA', 85 | 'state' => 'New York', 86 | 'state_code' => 'NY', 87 | 'country' => 'United States', 88 | 'country_code' => 'US' 89 | } 90 | ] 91 | ) 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@openbrewerydb.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | ### Added 10 | 11 | - Slugs 12 | - Authentication 13 | - Authorization 14 | - Brewery update suggestions feature 15 | - Added `exclude_types` brewery filter parameter 16 | - Updated sort parameter to change from +/- to asc/desc 17 | 18 | ## [0.4.2] - 2019-12-28 19 | 20 | ### Added 21 | 22 | - Breweries filtering by postal code: 'by_postal' 23 | 24 | ## [0.4.1] - 2019-03-07 25 | 26 | ### Fixed 27 | 28 | - Fix Brewery `by_state` filter to be more exact. Note: State abbreviations no longer work 29 | 30 | ### Removed 31 | 32 | - Ahoy analytics tracking 33 | - Unnecessary files 34 | 35 | ### Changed 36 | 37 | - Turned off IP spoofing check 38 | 39 | ## [0.4.0] - 2018-11-25 40 | 41 | ### Added 42 | 43 | - Brewery tags via ActsAsTaggableOn gem 44 | - This CHANGELOG 🎉 45 | 46 | ## [0.3.5] - 2018-11-24 47 | 48 | ### Changed 49 | 50 | - Security Issue: rack and loofah 51 | - Updated all gems to the most recent version 52 | - README build icon 53 | 54 | ## [0.3.4] - 2018-10-18 55 | 56 | ### Added 57 | 58 | - Community documentation for contribution. 59 | 60 | ### Removed 61 | 62 | - CircleCI config 63 | 64 | ## [0.3.3] - 2018-09-20 65 | 66 | ### Added 67 | 68 | - TravisCI config 69 | - README 70 | 71 | ## [0.3.2] - 2018-09-08 72 | 73 | ### Added 74 | 75 | - Sentry for error and bug tracking 76 | 77 | ### Changed 78 | 79 | - Redirect homepage to [documentation page](https://www.openbrewerydb.org) 80 | 81 | ### Removed 82 | 83 | - Rollbar because it was going to be \\$\\$\$ 84 | 85 | ## [0.3.1] - 2018-08-24 86 | 87 | ### Added 88 | 89 | - Rollbar for error and bug tracking 90 | 91 | ## [0.3.0] - 2018-08-23 92 | 93 | ### Added 94 | 95 | - Columns `country`, `longitude`, `latitude` to breweries table 96 | - Attach Geocoder gem to Brewery model 97 | - Task to update all brewery geocodes 98 | 99 | ### Changed 100 | 101 | - Rename breweries table `address` column to `street` 102 | 103 | ## [0.2.0] - 2018-08-11 104 | 105 | ### Added 106 | 107 | - Search implemented via ElasticSearch and Searchkick gem 108 | - Search endpoint 109 | - Autocomplete endpoint 110 | - Add event tracking via Ahoy Matey gem 111 | 112 | ### Changed 113 | 114 | - Disable Brewery POST, PUT, and DELETE endpoints for now 115 | 116 | ## 0.1.0 - 2018-06-29 117 | 118 | ### Added 119 | 120 | - Brewery and User models 121 | - Brewery and User CRUD endpoints 122 | - Breweries list endpoint with filtering by city, state, name, type 123 | - Single brewery endpoint 124 | - Pagination and sorting on breweries list 125 | - Take to import breweries based on Brewers Association website scrape 126 | - CircleCI configuration, Rubocop config, robots.txt 127 | 128 | [unreleased]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.4.1...HEAD 129 | [0.4.1]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.4.0...v0.4.1 130 | [0.4.0]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.3.5...v0.4.0 131 | [0.3.5]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.3.4...v0.3.5 132 | [0.3.4]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.3.3...v0.3.4 133 | [0.3.3]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.3.2...v0.3.3 134 | [0.3.2]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.3.1...v0.3.2 135 | [0.3.1]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.3.0...v0.3.1 136 | [0.3.0]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.2.0...v0.3.0 137 | [0.2.0]: https://github.com/chrisjm/openbrewerydb-api-server/compare/v0.1.0...v0.2.0 138 | -------------------------------------------------------------------------------- /app/services/update_state_abbreviations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Task to update the state abbreviations 4 | class UpdateStateAbbreviations 5 | def initialize 6 | @log = ActiveSupport::Logger.new('log/update_state_abbreviations.log') 7 | @dry_run = ENV['DRY_RUN'].present? ? ENV['DRY_RUN'].casecmp('true').zero? : false 8 | @counter = { updated: 0, skipped: 0, failed: 0, total: 0 } 9 | end 10 | 11 | def self.perform 12 | new.perform 13 | end 14 | 15 | def perform 16 | start_time = Time.now 17 | @log.info "Task started at #{start_time}" 18 | 19 | puts "\n!!!!! DRY RUN !!!!!\nNO DATA WILL BE CHANGED\n".yellow if @dry_run 20 | 21 | process_brewery_batches 22 | 23 | output_summary 24 | 25 | end_time = Time.now 26 | duration = (start_time - end_time).round(2).abs 27 | log_and_print("\nTask finished at #{end_time} and lasted #{duration}s.") 28 | @log.close 29 | end 30 | 31 | private 32 | 33 | def log_and_print(message) 34 | puts(message) 35 | @log.info(message.uncolorize) 36 | end 37 | 38 | def output_summary 39 | log_and_print("\n---------------\nTotal: #{@counter[:total]}".white) 40 | log_and_print("Updated: #{@counter[:updated]}".green) 41 | log_and_print("Skipped: #{@counter[:skipped]}".blue) 42 | log_and_print("Failed: #{@counter[:failed]}".red) 43 | log_and_print('----------------'.white) 44 | end 45 | 46 | def process_brewery_batches 47 | Brewery.find_in_batches.with_index do |group, batch| 48 | log_and_print("Processing group ##{batch + 1}") 49 | process_breweries(group) 50 | end 51 | end 52 | 53 | def process_breweries(breweries = []) 54 | breweries.each do |brewery| 55 | update_state_abbreviation(brewery) 56 | end 57 | end 58 | 59 | def update_state_abbreviation(brewery) 60 | @counter[:total] += 1 61 | 62 | if brewery.state.blank? 63 | @counter[:failed] += 1 64 | return 65 | end 66 | 67 | # Check if brewery state is only two characters (ie, an abbreviation) 68 | if brewery.state.match?(/^[A-Za-z]{2}$/) 69 | unless @dry_run 70 | brewery.update_attribute( 71 | :state_province, 72 | STATE_ABBR_TO_NAME[brewery.state_province.upcase] 73 | ) 74 | end 75 | @counter[:updated] += 1 76 | else 77 | @counter[:skipped] += 1 78 | end 79 | end 80 | end 81 | 82 | STATE_ABBR_TO_NAME = { 83 | 'AL' => 'Alabama', 84 | 'AK' => 'Alaska', 85 | 'AS' => 'America Samoa', 86 | 'AZ' => 'Arizona', 87 | 'AR' => 'Arkansas', 88 | 'CA' => 'California', 89 | 'CO' => 'Colorado', 90 | 'CT' => 'Connecticut', 91 | 'DE' => 'Delaware', 92 | 'DC' => 'District of Columbia', 93 | 'FM' => 'Federated States Of Micronesia', 94 | 'FL' => 'Florida', 95 | 'GA' => 'Georgia', 96 | 'GU' => 'Guam', 97 | 'HI' => 'Hawaii', 98 | 'ID' => 'Idaho', 99 | 'IL' => 'Illinois', 100 | 'IN' => 'Indiana', 101 | 'IA' => 'Iowa', 102 | 'KS' => 'Kansas', 103 | 'KY' => 'Kentucky', 104 | 'LA' => 'Louisiana', 105 | 'ME' => 'Maine', 106 | 'MH' => 'Marshall Islands', 107 | 'MD' => 'Maryland', 108 | 'MA' => 'Massachusetts', 109 | 'MI' => 'Michigan', 110 | 'MN' => 'Minnesota', 111 | 'MS' => 'Mississippi', 112 | 'MO' => 'Missouri', 113 | 'MT' => 'Montana', 114 | 'NE' => 'Nebraska', 115 | 'NV' => 'Nevada', 116 | 'NH' => 'New Hampshire', 117 | 'NJ' => 'New Jersey', 118 | 'NM' => 'New Mexico', 119 | 'NY' => 'New York', 120 | 'NC' => 'North Carolina', 121 | 'ND' => 'North Dakota', 122 | 'OH' => 'Ohio', 123 | 'OK' => 'Oklahoma', 124 | 'OR' => 'Oregon', 125 | 'PW' => 'Palau', 126 | 'PA' => 'Pennsylvania', 127 | 'PR' => 'Puerto Rico', 128 | 'RI' => 'Rhode Island', 129 | 'SC' => 'South Carolina', 130 | 'SD' => 'South Dakota', 131 | 'TN' => 'Tennessee', 132 | 'TX' => 'Texas', 133 | 'UT' => 'Utah', 134 | 'VT' => 'Vermont', 135 | 'VI' => 'Virgin Island', 136 | 'VA' => 'Virginia', 137 | 'WA' => 'Washington', 138 | 'WV' => 'West Virginia', 139 | 'WI' => 'Wisconsin', 140 | 'WY' => 'Wyoming' 141 | }.freeze 142 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 28 | # config.action_controller.asset_host = 'http://assets.example.com' 29 | 30 | # Specifies the header that your server uses for sending files. 31 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 32 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 33 | 34 | # Store uploaded files on the local file system (see config/storage.yml for options) 35 | config.active_storage.service = :local 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | config.force_ssl = true 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [:request_id] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "openbrewerydb-rest-api_#{Rails.env}" 58 | 59 | # config.action_mailer.default_url_options = { host: 'api.openbrewerydb.org' } 60 | # config.action_mailer.perform_caching = false 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation cannot be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Use a different logger for distributed setups. 77 | # require 'syslog/logger' 78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 79 | 80 | if ENV['RAILS_LOG_TO_STDOUT'].present? 81 | logger = ActiveSupport::Logger.new(STDOUT) 82 | logger.formatter = config.log_formatter 83 | config.logger = ActiveSupport::TaggedLogging.new(logger) 84 | end 85 | 86 | # Do not dump schema after migrations. 87 | config.active_record.dump_schema_after_migration = false 88 | 89 | Rails.application.routes.default_url_options = { 90 | host: 'api.openbrewerydb.org' 91 | } 92 | end 93 | -------------------------------------------------------------------------------- /config/environments/staging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 28 | # config.action_controller.asset_host = 'http://assets.example.com' 29 | 30 | # Specifies the header that your server uses for sending files. 31 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 32 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 33 | 34 | # Store uploaded files on the local file system (see config/storage.yml for options) 35 | config.active_storage.service = :local 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | config.force_ssl = false 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [:request_id] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "openbrewerydb-rest-api_#{Rails.env}" 58 | 59 | # config.action_mailer.default_url_options = { host: 'staging-api.openbrewerydb.org' } 60 | # config.action_mailer.perform_caching = false 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation cannot be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Use a different logger for distributed setups. 77 | # require 'syslog/logger' 78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 79 | 80 | if ENV['RAILS_LOG_TO_STDOUT'].present? 81 | logger = ActiveSupport::Logger.new(STDOUT) 82 | logger.formatter = config.log_formatter 83 | config.logger = ActiveSupport::TaggedLogging.new(logger) 84 | end 85 | 86 | # Do not dump schema after migrations. 87 | config.active_record.dump_schema_after_migration = false 88 | 89 | Rails.application.routes.default_url_options = { 90 | host: 'staging-api.openbrewerydb.org' 91 | } 92 | end 93 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Open Brewery DB 2 | 3 | ## Welcome to the Open Brewery DB community 4 | 5 | Thank you for contributing to Open Brewery DB! It's fellow beer lovers like you that make Open Brewery DB such a great resource. 🍻 6 | 7 | ## Why the guidelines 8 | 9 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 10 | 11 | ## What to contribute 12 | 13 | Open Brewery DB is a fully transparent, open source project and we love to receive any contributions from our community — you! There are many ways to contribute, from suggesting brewery updates, writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into Open Brewery DB itself. 14 | 15 | ## Responsibilities 16 | 17 | * Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 18 | * Keep feature versions as small as possible, preferably one new feature per version. 19 | * Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See the [Code of Conduct](CODE_OF_CONDUCT.md). 20 | 21 | Here are a couple of helpful tutorials: 22 | 23 | * [Make a Pull Request](http://makeapullrequest.com/) 24 | * [First Timers Only](http://www.firsttimersonly.com/) 25 | * [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 26 | 27 | ## Pull Request 28 | 29 | After you've created a branch on your fork with your changes, it's time to [make a pull request][pr-link]! 30 | 31 | Once you’ve submitted a pull request, the collaborators can review your proposed changes and decide whether or not to incorporate (pull in) your changes. 32 | 33 | ### Pull Request Pro Tips 34 | 35 | * [Fork][fork-link] the repository and [clone][clone-link] it locally. 36 | Connect your local repository to the original `upstream` repository by adding it as a [remote][remote-link]. 37 | Pull in changes from `upstream` often so that you stay up to date and so when you submit your pull request, 38 | merge conflicts will be less likely. See more detailed instructions [here][syncing-link]. 39 | * Create a [branch][branch-link] for your edits. 40 | * Contribute in the style of the project. This makes it easier for the collaborators to merge 41 | and for others to understand and maintain in the future. 42 | * Please try to squash all commits together before opening a pull request, but it's not currently required. If your pull request requires changes upon review, and you're already in the habit, please squash all additional commits as well. [This wiki page][squash-link] outlines the squash process. 43 | 44 | ### Open Pull Requests 45 | 46 | Once you’ve opened a pull request, a discussion will start around your proposed changes. 47 | 48 | Other contributors and users may chime in, but ultimately the decision is made by the collaborators. 49 | 50 | During the discussion, you may be asked to make some changes to your pull request. 51 | 52 | If so, add more commits to your branch and push them – they will automatically go into the existing pull request! 53 | 54 | Opening a pull request will trigger a Github Action build to check the validity of all links in the project. After the build completes, **please ensure that the build has passed**. If the build did not pass, please view the Github Action log and correct any errors that were found in your contribution. 55 | 56 | Thanks for being a part of this project, and we look forward to hearing from you soon! 🍻 57 | 58 | [branch-link]: 59 | [clone-link]: 60 | [fork-link]: 61 | [oauth-link]: 62 | [pr-link]: 63 | [remote-link]: 64 | [syncing-link]: 65 | [squash-link]: 66 | -------------------------------------------------------------------------------- /app/services/import/breweries.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Import 4 | # Brewery Import class 5 | class Breweries 6 | attr_reader :log 7 | 8 | def initialize 9 | @log = ActiveSupport::Logger.new('log/import_breweries.log') 10 | @dry_run = ENV['DRY_RUN'].present? ? ENV['DRY_RUN'].casecmp('true').zero? : false 11 | @counter = { added: 0, failed: 0, skipped: 0, total: 0 } 12 | @path_to_json = 'https://raw.githubusercontent.com/openbrewerydb/openbrewerydb/master/breweries.json' 13 | @path_to_sql = 'https://raw.githubusercontent.com/openbrewerydb/openbrewerydb/master/breweries.sql' 14 | end 15 | 16 | def self.perform 17 | new.perform 18 | end 19 | 20 | def perform 21 | start_time = Time.now 22 | puts "\nTask started at #{start_time}" 23 | @log.info "Task started at #{start_time}" 24 | 25 | puts "\n!!!!! DRY RUN !!!!!\nNO DATA WILL BE IMPORTED\n" if @dry_run 26 | 27 | if ENV['TRUNCATE']&.upcase == 'TRUE' 28 | import_breweries_sql 29 | else 30 | puts "Updating breweries\n" 31 | import_breweries 32 | end 33 | 34 | output_summary 35 | 36 | end_time = Time.now 37 | duration = (start_time - end_time).round(2).abs 38 | puts "\nTask finished at #{end_time} and lasted #{duration} seconds." 39 | @log.info "Task finished at #{end_time} and lasted #{duration} seconds." 40 | @log.close 41 | end 42 | 43 | private 44 | 45 | def import_breweries 46 | puts "#{Time.now} : Getting raw breweries file - json".blue 47 | connection = Faraday::Connection.new @path_to_json 48 | response = connection.get 49 | body = JSON.parse(response.body.as_json, symbolize_names: true) 50 | puts "#{Time.now} : Got file: #{response.status}".blue 51 | puts "#{Time.now} : Got #{body.size} breweries".blue 52 | 53 | if response.status != 200 54 | @log.info 'Could not get breweries. Exiting task' 55 | abort('Could not get breweries. Exiting task') 56 | end 57 | 58 | breweries = body.map do |brewery| 59 | @counter[:added] += 1 60 | 61 | { 62 | id: brewery[:id], 63 | name: brewery[:name], 64 | brewery_type: brewery[:brewery_type], 65 | address_1: brewery[:address_1], 66 | address_2: brewery[:address_2], 67 | address_3: brewery[:address_3], 68 | city: brewery[:city], 69 | state_province: brewery[:state_province], 70 | country: brewery[:country], 71 | postal_code: brewery[:postal_code], 72 | website_url: brewery[:website_url], 73 | phone: brewery[:phone], 74 | longitude: brewery[:longitude], 75 | latitude: brewery[:latitude] 76 | } 77 | end 78 | 79 | if breweries.empty? 80 | puts "#{Time.now} : No breweries to map, exiting" 81 | abort('No breweries to map, exiting') 82 | end 83 | 84 | # filter out nils. In ruby-2.7, we can use filter_map instead of map above. 85 | breweries.filter!(&:present?) 86 | 87 | puts "#{Time.now} : Mapped breweries".green 88 | 89 | if @dry_run 90 | @counter[:added] = 0 91 | @counter[:skipped] += breweries.size 92 | else 93 | puts "#{Time.now} : Saving breweries".blue 94 | Brewery.upsert_all(breweries, unique_by: :id) 95 | end 96 | end 97 | 98 | def import_breweries_sql 99 | puts "#{Time.now} : Getting raw breweries file - sql".blue 100 | connection = Faraday::Connection.new @path_to_sql 101 | response = connection.get 102 | puts "#{Time.now} : Got file: #{response.status}".blue 103 | 104 | unless @dry_run 105 | puts "#{Time.now} : Truncating table before import".blue 106 | ActiveRecord::Base.connection.truncate(:breweries) 107 | end 108 | puts "#{Time.now} : # of Breweries (before import): #{Brewery.count}".green 109 | 110 | unless @dry_run 111 | puts "#{Time.now} : Inserting to Breweries by SQL" 112 | ActiveRecord::Base.connection.insert(response.body.to_s) 113 | end 114 | 115 | # check db 116 | @counter[:added] = Brewery.count 117 | puts "#{Time.now} : # of Breweries (after import): #{@counter[:added]}".green 118 | end 119 | 120 | def output_summary 121 | puts "\n---------------\nTotal: #{@counter[:total]}".white 122 | puts "Added: #{@counter[:added]}".green 123 | puts "Skipped: #{@counter[:skipped]}".blue 124 | puts "Failed: #{@counter[:failed]}".red 125 | puts '----------------'.white 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rails generate rspec:install` command. 4 | # Conventionally, all 5 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 6 | # The generated `.rspec` file contains `--require spec_helper` which will cause 7 | # this file to always be loaded, without a need to explicitly require it in any 8 | # files. 9 | # 10 | # Given that it is always loaded, you are encouraged to keep this file as 11 | # light-weight as possible. Requiring heavyweight dependencies from this file 12 | # will add to the boot time of your test suite on EVERY test run, even for an 13 | # individual file that may not need all of that loaded. Instead, consider making 14 | # a separate helper file that requires the additional dependencies and performs 15 | # the additional setup, and require it from the spec files that actually need 16 | # it. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 44 | # have no way to turn it off -- the option exists only for backwards 45 | # compatibility in RSpec 3). It causes shared context metadata to be 46 | # inherited by the metadata hash of host groups and examples, rather than 47 | # triggering implicit auto-inclusion in groups with matching metadata. 48 | config.shared_context_metadata_behavior = :apply_to_host_groups 49 | 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # Many RSpec users commonly either run the entire suite or an individual 70 | # file, and it's useful to allow more verbose output when running an 71 | # individual spec file. 72 | if config.files_to_run.one? 73 | # Use the documentation formatter for detailed output, 74 | # unless a formatter has already been configured 75 | # (e.g. via a command-line flag). 76 | config.default_formatter = "doc" 77 | end 78 | 79 | # Print the 10 slowest examples and example groups at the 80 | # end of the spec run, to help surface which specs are running 81 | # particularly slow. 82 | config.profile_examples = 5 83 | 84 | # Run specs in random order to surface order dependencies. If you find an 85 | # order dependency and want to debug it, you can fix the order by providing 86 | # the seed, which is printed after each run. 87 | # --seed 1234 88 | config.order = :random 89 | 90 | # Seed global randomization in this process using the `--seed` CLI option. 91 | # Setting this allows you to use `--seed` to deterministically reproduce 92 | # test failures related to randomization by passing the same `--seed` value 93 | # as the one that triggered the failure. 94 | Kernel.srand config.seed 95 | end 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍻 Official Open Brewery DB REST API Server 2 | 3 | ![Open Brewery DB Logo](obdb-logo-md.jpg) 4 | 5 | ![Github Actions Badge](https://github.com/chrisjm/openbrewerydb-rails-api/workflows/Build%20&%20Test%20Suite/badge.svg) 6 | 7 | The Open Brewery DB API server is a Ruby on Rails app connected to a PostgreSQL DB server served at https://api.openbrewerydb.org. 8 | 9 | [Documentation](https://www.openbrewerydb.org/) 10 | 11 | ## 📦 Dependencies 12 | 13 | - Ruby 3.2.0 14 | - PostgreSQL 14.2 15 | - Elastic Search (See [Searchkick's](https://github.com/ankane/searchkick) [Getting Started](https://github.com/ankane/searchkick#getting-started) section.) _Note: Elastic Search is likely to be removed in the future._ 16 | 17 | ## 🚀 Getting Started 18 | 19 | ### Local Environement 20 | 21 | 1. Clone the repo `$ git clone https://github.com/openbrewerydb/openbrewerydb-rest-api` 22 | 2. Run `bundle install` 23 | 3. Run `bundle exec rails db:setup` 24 | 4. Run database import. See [Data Import Task](#Data-Import-task) 25 | 5. Run `bundle exec rails s` 26 | 27 | The server will be running at `http://localhost:3000` 28 | 29 | #### Updating 30 | 31 | **NOTE: Don't forget to use `bundle lock --add-platform x86_64-linux` after updating bundle!** 32 | 33 | ### Database Expectations 34 | 35 | There are some assumptions for the local PostgreSQL service configuration. 36 | 37 | - Host is `localhost` or `127.0.0.1` 38 | - User is blank (i.e. it is the current system user) 39 | - Password is blank 40 | 41 | All of these settings can be overwritten by setting environment variables in `.env`: 42 | 43 | - `POSTGRES_USER` 44 | - `POSTGRES_PASSWORD` 45 | - `POSTGRES_HOST` 46 | 47 | See `config/database.yml` for configuration. 48 | 49 | ## Data Import task 50 | 51 | Using Rake, we can import the brewery data with a simple command. 52 | 53 | `rake breweries:import:breweries` 54 | 55 | This command will update your existing database in the development environment with new breweries. We also allow you to set environment variables to determine how to run the task. 56 | 57 | - `RAILS_ENV=[development, test]` 58 | - `TRUNCATE=[true, false]` 59 | - `DRY_RUN=[true, false]` 60 | 61 | ### Prerequisites 62 | 63 | You will need to have your postgresql server running as well as the ElasticSearch container. ElasticSearch is required for the autocompletion and fuzzy searching functionalities. 64 | 65 | ```shell 66 | #start postgresql server 67 | sudo service postgresql start 68 | 69 | #pull and start elasticsearch container 70 | docker pull docker.elastic.co/elasticsearch/elasticsearch:7.12.1 71 | docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.12.1 72 | ``` 73 | 74 | Additionally, these commands will not run Geocode or index fields with `searchkick`. These have their own rake tasks and will need to be run manually if you would like to use their functionality. 75 | 76 | ```shell 77 | #geocode 78 | rake geocode:all CLASS=Brewery RAILS_ENV=[development, test] BATCH=100 79 | 80 | #searchkick 81 | rake searchkick:reindex CLASS=Brewery RAILS_ENV=[development, test] 82 | ``` 83 | 84 | > Note! The geocode task takes an extraordinary amount of time. Do not run this task unless you require it. You should also use the BATCH env so you do not run out of memory. 85 | 86 | ### Update 87 | 88 | Updating is the default task. This should be run if you wish to only update your existing database. 89 | 90 | ### Fresh import 91 | 92 | This option will truncate your breweries table to make sure it's clean and then add all breweries. This will also bypass all validations as it's inserting by direct SQL commands. 93 | 94 | `rake breweries:import:breweries TRUNCATE=true RAILS_ENV=[development, test]` 95 | 96 | ### Adding data manually 97 | 98 | Importing data can also be done manually if you grab the data from [openbrewerydb](https://github.com/openbrewerydb/openbrewerydb). 99 | 100 | - Open postgresql shell with `psql ` 101 | - `\c openbrewerydb_development` to connect to the database 102 | - Make sure the `breweries` table exists. `\dt` 103 | - Copy the data to the table: `\copy breweries(id,name,brewery_type,address_1,address_2,address_3,city,state_province,postal_code,website_url,phone,created_at,updated_at,country,longitude,latitude,tags) from '' DELIMITER ',' CSV HEADER` 104 | - Run `SELECT * FROM breweries LIMIT 10;` to make sure data loaded 105 | 106 | ### Running Tests 107 | 108 | `bundle exec rake` or `bundle exec rspec` 109 | 110 | ## 🤝 Contributing 111 | 112 | For information on contributing to this project, please see the [contributing guide](CONTRIBUTING.md) and our [code of conduct](CODE_OF_CONDUCT.md). 113 | 114 | ## 🔗 Related 115 | 116 | - [Open Brewery DB Website & Documentation](https://github.com/chrisjm/openbrewerydb-gatsby) 117 | - [Open Brewery DB Dataset](https://github.com/openbrewerydb/openbrewerydb) 118 | 119 | ## 👾 Community 120 | 121 | - [Join the Newsletter](http://eepurl.com/dBjS0j) 122 | - [Join the Discord](https://discord.gg/SHtpdEN) 123 | 124 | ## 📫 Feedback 125 | 126 | Any feedback, please [email me](mailto:chris@openbrewerydb.org). 127 | 128 | Cheers! 🍻 129 | -------------------------------------------------------------------------------- /app/controllers/v1/breweries/breweries_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module V1 4 | module Breweries 5 | # BreweryController class 6 | class BreweriesController < ApplicationController 7 | SORT_ORDER = %w[asc desc].freeze 8 | DDOS_ATTACK = ENV['DDOS_ATTACK'] == 'true' 9 | 10 | before_action :set_brewery, only: %i[show update destroy] 11 | before_action :validate_params, only: %i[index meta autocomplete search] 12 | 13 | # FILTER: /breweries?by_country=scotland 14 | has_scope :by_country, only: %i[index meta] 15 | # FILTER: /breweries?by_city=san%20diego 16 | has_scope :by_city, only: %i[index meta] 17 | # FILTER: /breweries?by_name=almanac 18 | has_scope :by_name, only: %i[index meta] 19 | # FILTER: /breweries?by_state=california 20 | has_scope :by_state, only: %i[index meta] 21 | # FILTER: /breweries?by_type=micro 22 | has_scope :by_type, only: %i[index meta] 23 | # FILTER /breweries?by_postal=44107 24 | has_scope :by_postal, only: %i[index meta] 25 | # FILTER /breweries?by_ids=1,2,3 26 | has_scope :by_ids, only: %i[index meta] 27 | # FILTER /breweries?by_dist=38.8977,77.0365 28 | has_scope :by_dist, only: %i[index meta] 29 | # FILTER /breweries?exclude_types=micro,nano 30 | has_scope :exclude_types, only: %i[index meta] 31 | 32 | # GET /breweries 33 | def index 34 | expires_in 1.day, public: true 35 | @breweries = 36 | apply_scopes(Brewery) 37 | .order(order_params) 38 | .page(params[:page]) 39 | .per(params[:per_page]) 40 | json_response(@breweries) 41 | end 42 | 43 | # GET /breweries/meta 44 | def meta 45 | expires_in 1.day, public: true 46 | 47 | @breweries = apply_scopes(Brewery).all 48 | 49 | # Returning everything as string for now because Ruby is annoying 50 | json_response({ 51 | total: @breweries.size.to_s, 52 | page: params[:page] || 1.to_s, 53 | per_page: params[:per_page] || Kaminari.config.default_per_page.to_s 54 | }) 55 | end 56 | 57 | # GET /breweries/:id 58 | def show 59 | expires_in 1.day, public: true 60 | json_response(@brewery) 61 | end 62 | 63 | # GET /breweries/random 64 | def random 65 | expires_in 1.day, public: true 66 | size = params[:size].to_i.zero? ? 1 : params[:size].to_i 67 | 68 | # ActiveRecord random record: https://stackoverflow.com/a/25577054 69 | @breweries = Brewery.order(Arel.sql('RANDOM()')).limit([size, MAX_PER_PAGE].min) 70 | 71 | json_response(@breweries) 72 | end 73 | 74 | # GET /breweries/autocomplete 75 | def autocomplete 76 | expires_in 1.day, public: true 77 | 78 | @breweries = 79 | if DDOS_ATTACK 80 | { message: 'This endpoint is temporarily disabled.' } 81 | else 82 | Brewery.search( 83 | params[:query], 84 | limit: 15, 85 | misspellings: { below: 2 } 86 | ).map { |b| { id: b.id, name: b.name } } 87 | end 88 | json_response(@breweries) 89 | end 90 | 91 | # GET /breweries/search 92 | def search 93 | expires_in 1.day, public: true 94 | 95 | @breweries = 96 | if DDOS_ATTACK 97 | { message: 'This endpoint is temporarily disabled.' } 98 | else 99 | Brewery.search( 100 | params[:query], 101 | page: params[:page], 102 | per_page: params[:per_page] 103 | ) 104 | end 105 | 106 | json_response(@breweries) 107 | end 108 | 109 | private 110 | 111 | def brewery_params 112 | params.permit( 113 | :name, :address_1, :city, :state_province, :postal_code, :phone, :country, 114 | :website_url, :brewery_type 115 | ) 116 | end 117 | 118 | # A list of the param names that can be used for ordering the model list. 119 | # For example it retrieves a list of breweries in descending order of type. 120 | # Within a specific type, names are ordered first 121 | # order_params 122 | # GET /breweries?sort=type_desc,name 123 | # order_params # => { brewery_type: :desc, name: :asc } 124 | # Brewery.order(brewery_type: :desc, name: :asc) 125 | # 126 | def order_params 127 | return unless params[:sort] 128 | 129 | ordering = {} 130 | sorted_params = params[:sort].split(',') 131 | 132 | sorted_params.each do |attr| 133 | attr, sort_m = attr.split(':') 134 | sort_method = SORT_ORDER.include?(sort_m) ? sort_m : 'asc' 135 | attr = 'brewery_type' if attr == 'type' 136 | 137 | ordering[attr] = sort_method.to_sym if Brewery.attribute_names.include?(attr) 138 | end 139 | 140 | ordering 141 | end 142 | 143 | def set_brewery 144 | @brewery = Brewery.find_by!(id: params[:id]) 145 | end 146 | 147 | def validate_params 148 | errors = [] 149 | params.each do |key, value| 150 | next if %w[controller action].include?(key) 151 | 152 | # Convert underscores to spaces in params (for convenience) 153 | value.gsub!('_', ' ') unless value.nil? || !value.instance_of?(String) 154 | 155 | case key 156 | when 'by_type' 157 | unless BREWERY_TYPES.include?(value) 158 | errors.push("Brewery type must include one of these types: #{BREWERY_TYPES}") 159 | end 160 | when 'by_dist' 161 | unless value.split(',').map(&:to_f).size == 2 162 | errors.push("You must provide latitude and longitude for the 'by_dist' query param") 163 | end 164 | end 165 | end 166 | 167 | return json_response({ errors: }, :bad_request) if errors.size.positive? 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (6.1.7.7) 5 | actionpack (= 6.1.7.7) 6 | activesupport (= 6.1.7.7) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (6.1.7.7) 10 | actionpack (= 6.1.7.7) 11 | activejob (= 6.1.7.7) 12 | activerecord (= 6.1.7.7) 13 | activestorage (= 6.1.7.7) 14 | activesupport (= 6.1.7.7) 15 | mail (>= 2.7.1) 16 | actionmailer (6.1.7.7) 17 | actionpack (= 6.1.7.7) 18 | actionview (= 6.1.7.7) 19 | activejob (= 6.1.7.7) 20 | activesupport (= 6.1.7.7) 21 | mail (~> 2.5, >= 2.5.4) 22 | rails-dom-testing (~> 2.0) 23 | actionpack (6.1.7.7) 24 | actionview (= 6.1.7.7) 25 | activesupport (= 6.1.7.7) 26 | rack (~> 2.0, >= 2.0.9) 27 | rack-test (>= 0.6.3) 28 | rails-dom-testing (~> 2.0) 29 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 30 | actiontext (6.1.7.7) 31 | actionpack (= 6.1.7.7) 32 | activerecord (= 6.1.7.7) 33 | activestorage (= 6.1.7.7) 34 | activesupport (= 6.1.7.7) 35 | nokogiri (>= 1.8.5) 36 | actionview (6.1.7.7) 37 | activesupport (= 6.1.7.7) 38 | builder (~> 3.1) 39 | erubi (~> 1.4) 40 | rails-dom-testing (~> 2.0) 41 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 42 | active_model_serializers (0.10.13) 43 | actionpack (>= 4.1, < 7.1) 44 | activemodel (>= 4.1, < 7.1) 45 | case_transform (>= 0.2) 46 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3) 47 | activejob (6.1.7.7) 48 | activesupport (= 6.1.7.7) 49 | globalid (>= 0.3.6) 50 | activemodel (6.1.7.7) 51 | activesupport (= 6.1.7.7) 52 | activerecord (6.1.7.7) 53 | activemodel (= 6.1.7.7) 54 | activesupport (= 6.1.7.7) 55 | activestorage (6.1.7.7) 56 | actionpack (= 6.1.7.7) 57 | activejob (= 6.1.7.7) 58 | activerecord (= 6.1.7.7) 59 | activesupport (= 6.1.7.7) 60 | marcel (~> 1.0) 61 | mini_mime (>= 1.1.0) 62 | activesupport (6.1.7.7) 63 | concurrent-ruby (~> 1.0, >= 1.0.2) 64 | i18n (>= 1.6, < 2) 65 | minitest (>= 5.1) 66 | tzinfo (~> 2.0) 67 | zeitwerk (~> 2.3) 68 | ast (2.4.2) 69 | bcrypt (3.1.18) 70 | bootsnap (1.16.0) 71 | msgpack (~> 1.2) 72 | builder (3.2.4) 73 | byebug (11.1.3) 74 | case_transform (0.2) 75 | activesupport 76 | cloudflare-rails (2.4.0) 77 | actionpack (>= 5.2, < 7.1.0) 78 | activesupport (>= 5.2, < 7.1.0) 79 | railties (>= 5.2, < 7.1.0) 80 | coderay (1.1.3) 81 | colorize (0.8.1) 82 | concurrent-ruby (1.2.3) 83 | crass (1.0.6) 84 | database_cleaner (2.0.2) 85 | database_cleaner-active_record (>= 2, < 3) 86 | database_cleaner-active_record (2.1.0) 87 | activerecord (>= 5.a) 88 | database_cleaner-core (~> 2.0.0) 89 | database_cleaner-core (2.0.1) 90 | date (3.3.4) 91 | diff-lcs (1.5.0) 92 | docile (1.4.0) 93 | dotenv (2.8.1) 94 | dotenv-rails (2.8.1) 95 | dotenv (= 2.8.1) 96 | railties (>= 3.2) 97 | elasticsearch (7.13.3) 98 | elasticsearch-api (= 7.13.3) 99 | elasticsearch-transport (= 7.13.3) 100 | elasticsearch-api (7.13.3) 101 | multi_json 102 | elasticsearch-transport (7.13.3) 103 | faraday (~> 1) 104 | multi_json 105 | erubi (1.12.0) 106 | factory_bot (6.2.1) 107 | activesupport (>= 5.0.0) 108 | factory_bot_rails (6.2.0) 109 | factory_bot (~> 6.2.0) 110 | railties (>= 5.0.0) 111 | faker (2.23.0) 112 | i18n (>= 1.8.11, < 2) 113 | faraday (1.10.3) 114 | faraday-em_http (~> 1.0) 115 | faraday-em_synchrony (~> 1.0) 116 | faraday-excon (~> 1.1) 117 | faraday-httpclient (~> 1.0) 118 | faraday-multipart (~> 1.0) 119 | faraday-net_http (~> 1.0) 120 | faraday-net_http_persistent (~> 1.0) 121 | faraday-patron (~> 1.0) 122 | faraday-rack (~> 1.0) 123 | faraday-retry (~> 1.0) 124 | ruby2_keywords (>= 0.0.4) 125 | faraday-em_http (1.0.0) 126 | faraday-em_synchrony (1.0.0) 127 | faraday-excon (1.1.0) 128 | faraday-httpclient (1.0.1) 129 | faraday-multipart (1.0.4) 130 | multipart-post (~> 2) 131 | faraday-net_http (1.0.1) 132 | faraday-net_http_persistent (1.2.0) 133 | faraday-patron (1.0.0) 134 | faraday-rack (1.0.0) 135 | faraday-retry (1.0.3) 136 | ffi (1.15.5) 137 | formatador (1.1.0) 138 | geocoder (1.8.1) 139 | geokit (1.14.0) 140 | geokit-rails (2.5.0) 141 | geokit (~> 1.5) 142 | rails (>= 3.0) 143 | globalid (1.2.1) 144 | activesupport (>= 6.1) 145 | guard (2.18.0) 146 | formatador (>= 0.2.4) 147 | listen (>= 2.7, < 4.0) 148 | lumberjack (>= 1.0.12, < 2.0) 149 | nenv (~> 0.1) 150 | notiffany (~> 0.0) 151 | pry (>= 0.13.0) 152 | shellany (~> 0.0) 153 | thor (>= 0.18.1) 154 | guard-compat (1.2.1) 155 | guard-rspec (4.7.3) 156 | guard (~> 2.1) 157 | guard-compat (~> 1.1) 158 | rspec (>= 2.99.0, < 4.0) 159 | has_scope (0.8.1) 160 | actionpack (>= 5.2) 161 | activesupport (>= 5.2) 162 | hashie (5.0.0) 163 | i18n (1.14.1) 164 | concurrent-ruby (~> 1.0) 165 | json (2.6.3) 166 | jsonapi-renderer (0.2.2) 167 | kaminari (1.2.2) 168 | activesupport (>= 4.1.0) 169 | kaminari-actionview (= 1.2.2) 170 | kaminari-activerecord (= 1.2.2) 171 | kaminari-core (= 1.2.2) 172 | kaminari-actionview (1.2.2) 173 | actionview 174 | kaminari-core (= 1.2.2) 175 | kaminari-activerecord (1.2.2) 176 | activerecord 177 | kaminari-core (= 1.2.2) 178 | kaminari-core (1.2.2) 179 | listen (3.8.0) 180 | rb-fsevent (~> 0.10, >= 0.10.3) 181 | rb-inotify (~> 0.9, >= 0.9.10) 182 | loofah (2.22.0) 183 | crass (~> 1.0.2) 184 | nokogiri (>= 1.12.0) 185 | lumberjack (1.2.8) 186 | mail (2.8.1) 187 | mini_mime (>= 0.1.1) 188 | net-imap 189 | net-pop 190 | net-smtp 191 | marcel (1.0.2) 192 | method_source (1.0.0) 193 | mini_mime (1.1.5) 194 | minitest (5.22.2) 195 | msgpack (1.6.1) 196 | multi_json (1.15.0) 197 | multipart-post (2.3.0) 198 | nenv (0.3.0) 199 | net-imap (0.4.10) 200 | date 201 | net-protocol 202 | net-pop (0.1.2) 203 | net-protocol 204 | net-protocol (0.2.2) 205 | timeout 206 | net-smtp (0.4.0.1) 207 | net-protocol 208 | nio4r (2.7.0) 209 | nokogiri (1.15.6-x86_64-darwin) 210 | racc (~> 1.4) 211 | nokogiri (1.15.6-x86_64-linux) 212 | racc (~> 1.4) 213 | notiffany (0.1.3) 214 | nenv (~> 0.1) 215 | shellany (~> 0.0) 216 | parallel (1.22.1) 217 | parser (3.2.1.1) 218 | ast (~> 2.4.1) 219 | pg (1.4.6) 220 | pry (0.14.2) 221 | coderay (~> 1.1) 222 | method_source (~> 1.0) 223 | puma (5.6.8) 224 | nio4r (~> 2.0) 225 | racc (1.7.3) 226 | rack (2.2.8.1) 227 | rack-cors (1.1.1) 228 | rack (>= 2.0.0) 229 | rack-test (2.1.0) 230 | rack (>= 1.3) 231 | rails (6.1.7.7) 232 | actioncable (= 6.1.7.7) 233 | actionmailbox (= 6.1.7.7) 234 | actionmailer (= 6.1.7.7) 235 | actionpack (= 6.1.7.7) 236 | actiontext (= 6.1.7.7) 237 | actionview (= 6.1.7.7) 238 | activejob (= 6.1.7.7) 239 | activemodel (= 6.1.7.7) 240 | activerecord (= 6.1.7.7) 241 | activestorage (= 6.1.7.7) 242 | activesupport (= 6.1.7.7) 243 | bundler (>= 1.15.0) 244 | railties (= 6.1.7.7) 245 | sprockets-rails (>= 2.0.0) 246 | rails-dom-testing (2.2.0) 247 | activesupport (>= 5.0.0) 248 | minitest 249 | nokogiri (>= 1.6) 250 | rails-html-sanitizer (1.6.0) 251 | loofah (~> 2.21) 252 | nokogiri (~> 1.14) 253 | railties (6.1.7.7) 254 | actionpack (= 6.1.7.7) 255 | activesupport (= 6.1.7.7) 256 | method_source 257 | rake (>= 12.2) 258 | thor (~> 1.0) 259 | rainbow (3.1.1) 260 | rake (13.1.0) 261 | rb-fsevent (0.11.2) 262 | rb-inotify (0.10.1) 263 | ffi (~> 1.0) 264 | regexp_parser (2.7.0) 265 | rexml (3.2.5) 266 | rspec (3.12.0) 267 | rspec-core (~> 3.12.0) 268 | rspec-expectations (~> 3.12.0) 269 | rspec-mocks (~> 3.12.0) 270 | rspec-core (3.12.1) 271 | rspec-support (~> 3.12.0) 272 | rspec-expectations (3.12.2) 273 | diff-lcs (>= 1.2.0, < 2.0) 274 | rspec-support (~> 3.12.0) 275 | rspec-mocks (3.12.4) 276 | diff-lcs (>= 1.2.0, < 2.0) 277 | rspec-support (~> 3.12.0) 278 | rspec-rails (5.1.2) 279 | actionpack (>= 5.2) 280 | activesupport (>= 5.2) 281 | railties (>= 5.2) 282 | rspec-core (~> 3.10) 283 | rspec-expectations (~> 3.10) 284 | rspec-mocks (~> 3.10) 285 | rspec-support (~> 3.10) 286 | rspec-support (3.12.0) 287 | rspec_junit_formatter (0.6.0) 288 | rspec-core (>= 2, < 4, != 2.12.0) 289 | rubocop (1.48.1) 290 | json (~> 2.3) 291 | parallel (~> 1.10) 292 | parser (>= 3.2.0.0) 293 | rainbow (>= 2.2.2, < 4.0) 294 | regexp_parser (>= 1.8, < 3.0) 295 | rexml (>= 3.2.5, < 4.0) 296 | rubocop-ast (>= 1.26.0, < 2.0) 297 | ruby-progressbar (~> 1.7) 298 | unicode-display_width (>= 2.4.0, < 3.0) 299 | rubocop-ast (1.28.0) 300 | parser (>= 3.2.1.0) 301 | rubocop-capybara (2.17.1) 302 | rubocop (~> 1.41) 303 | rubocop-rails (2.18.0) 304 | activesupport (>= 4.2.0) 305 | rack (>= 1.1) 306 | rubocop (>= 1.33.0, < 2.0) 307 | rubocop-rspec (2.19.0) 308 | rubocop (~> 1.33) 309 | rubocop-capybara (~> 2.17) 310 | ruby-progressbar (1.13.0) 311 | ruby2_keywords (0.0.5) 312 | searchkick (4.6.3) 313 | activemodel (>= 5) 314 | elasticsearch (>= 6, < 7.14) 315 | hashie 316 | sentry-rails (5.8.0) 317 | railties (>= 5.0) 318 | sentry-ruby (~> 5.8.0) 319 | sentry-ruby (5.8.0) 320 | concurrent-ruby (~> 1.0, >= 1.0.2) 321 | shellany (0.0.1) 322 | shoulda-matchers (3.1.3) 323 | activesupport (>= 4.0.0) 324 | simplecov (0.22.0) 325 | docile (~> 1.1) 326 | simplecov-html (~> 0.11) 327 | simplecov_json_formatter (~> 0.1) 328 | simplecov-html (0.12.3) 329 | simplecov_json_formatter (0.1.4) 330 | spring (2.1.1) 331 | spring-watcher-listen (2.0.1) 332 | listen (>= 2.7, < 4.0) 333 | spring (>= 1.2, < 3.0) 334 | sprockets (4.2.1) 335 | concurrent-ruby (~> 1.0) 336 | rack (>= 2.2.4, < 4) 337 | sprockets-rails (3.4.2) 338 | actionpack (>= 5.2) 339 | activesupport (>= 5.2) 340 | sprockets (>= 3.0.0) 341 | thor (1.3.1) 342 | timeout (0.4.1) 343 | tzinfo (2.0.6) 344 | concurrent-ruby (~> 1.0) 345 | unicode-display_width (2.4.2) 346 | websocket-driver (0.7.6) 347 | websocket-extensions (>= 0.1.0) 348 | websocket-extensions (0.1.5) 349 | zeitwerk (2.6.13) 350 | 351 | PLATFORMS 352 | x86_64-darwin-22 353 | x86_64-linux 354 | 355 | DEPENDENCIES 356 | active_model_serializers (~> 0.10) 357 | bcrypt (~> 3.1) 358 | bootsnap (>= 1.1) 359 | byebug 360 | cloudflare-rails (~> 2.0) 361 | colorize (~> 0.8) 362 | database_cleaner (~> 2.0) 363 | dotenv-rails (~> 2.7) 364 | factory_bot_rails (~> 6.2) 365 | faker (~> 2.17) 366 | faraday (~> 1.4) 367 | geocoder (~> 1.6) 368 | geokit-rails (~> 2.3) 369 | guard-rspec (~> 4.7) 370 | has_scope (~> 0.8) 371 | kaminari (~> 1.2) 372 | listen (~> 3.2) 373 | nokogiri (~> 1.15) 374 | pg (~> 1.4) 375 | puma (~> 5.6) 376 | rack (>= 2.2.6) 377 | rack-cors (~> 1.1) 378 | rails (~> 6.1) 379 | rexml 380 | rspec-rails (~> 5.0) 381 | rspec_junit_formatter (~> 0.4) 382 | rubocop-rails (~> 2.10) 383 | rubocop-rspec (~> 2.3) 384 | searchkick (~> 4.6) 385 | sentry-rails 386 | shoulda-matchers (~> 3.1) 387 | simplecov (~> 0.21) 388 | spring (~> 2.1) 389 | spring-watcher-listen (~> 2.0) 390 | tzinfo-data 391 | 392 | RUBY VERSION 393 | ruby 3.2.0p0 394 | 395 | BUNDLED WITH 396 | 2.4.2 397 | -------------------------------------------------------------------------------- /spec/requests/breweries_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # NOTE: The `/breweries` endpoint will be deprecrecated. Please use `/v1/breweries` instead. 4 | 5 | require "rails_helper" 6 | 7 | RSpec.describe "Breweries API" do 8 | describe "GET /breweries" do 9 | context "when no params are passed" do 10 | before do 11 | create_list(:brewery, 201) 12 | get "/breweries" 13 | end 14 | 15 | # NOTE: Set in /config/initializers/kaminari_config.rb 16 | it "returns the default number of breweries" do 17 | expect(json.size).to eq(50) 18 | end 19 | 20 | it "returns status code 200" do 21 | expect(response).to have_http_status(:ok) 22 | end 23 | 24 | it "returns Cache-Control headers" do 25 | expect(response.headers["Cache-Control"]).to eq( 26 | "max-age=86400, public" 27 | ) 28 | end 29 | end 30 | 31 | context "when invalid params are passed" do 32 | before do 33 | create_list(:brewery, 51) 34 | get "/breweries", params: { 35 | by_state: nil, 36 | page: "invalid", 37 | sort: "*bob", 38 | id: 42, 39 | brewery: { id: 42 } 40 | } 41 | end 42 | 43 | it "returns a status of 200" do 44 | expect(response).to have_http_status(:ok) 45 | end 46 | 47 | it "returns the default number of breweries" do 48 | expect(json.size).to eq(50) 49 | end 50 | end 51 | 52 | context "when page param is passed" do 53 | before do 54 | create_list(:brewery, 101) 55 | get "/breweries", params: { page: 3 } 56 | end 57 | 58 | it "returns another page of breweries" do 59 | expect(json.size).to eq(1) 60 | end 61 | end 62 | 63 | context "when per_page param is passed" do 64 | before do 65 | create_list(:brewery, 205) 66 | end 67 | 68 | it "returns a limited number breweries" do 69 | get "/breweries", params: { per_page: 5 } 70 | expect(json.size).to eq(5) 71 | end 72 | 73 | # NOTE: Set in /config/initializers/kaminari_config.rb 74 | it "does not exceed the maximum number of breweries per page" do 75 | get "/breweries", params: { per_page: 201 } 76 | expect(json.size).to eq(200) 77 | end 78 | end 79 | 80 | context "when by_city param is passed" do 81 | before do 82 | create_list(:brewery, 1) 83 | create_list(:brewery, 1, city: "San Diego") 84 | create_list(:brewery, 1, city: "San Francisco") 85 | create_list(:brewery, 1, city: "Houston") 86 | end 87 | 88 | it "returns a filtered list of breweries" do 89 | get "/breweries", params: { by_city: "Houston" } 90 | expect(json.size).to eq(1) 91 | end 92 | 93 | it "returns a filtered list with multiple" do 94 | get "/breweries", params: { by_city: "San" } 95 | expect(json.size).to eq(2) 96 | end 97 | 98 | it "returns a filtered list with + as space" do 99 | get "/breweries", params: { by_city: "San+Diego" } 100 | expect(json.size).to eq(1) 101 | end 102 | end 103 | 104 | context "when by_country param is passed" do 105 | before do 106 | create_list(:brewery, 1, country: "England") 107 | create_list(:brewery, 1, country: "South Korea") 108 | end 109 | 110 | it "returns a filtered list of breweries" do 111 | get "/breweries", params: { by_country: "England" } 112 | expect(json.size).to eq(1) 113 | end 114 | 115 | it "handles '+' as a space in the query string" do 116 | get "/breweries", params: { by_country: "South+Korea" } 117 | expect(json.size).to eq(1) 118 | end 119 | end 120 | 121 | context "when by_name param is passed" do 122 | before do 123 | create_list(:brewery, 1, name: "Broad Brook Brewing Company") 124 | create_list(:brewery, 1, name: "McHappy's Brewpub Extravaganza") 125 | end 126 | 127 | it "returns a filtered list of breweries" do 128 | get "/breweries", params: { by_name: "mchappy" } 129 | expect(json.size).to eq(1) 130 | end 131 | 132 | it "handles '+' as a space in the query string" do 133 | get "/breweries", params: { by_name: "broad+brook" } 134 | expect(json.size).to eq(1) 135 | end 136 | 137 | it "handles a space as a space in the query string" do 138 | get "/breweries", params: { by_name: "broad brook" } 139 | expect(json.size).to eq(1) 140 | end 141 | end 142 | 143 | context "when by_state param is passed" do 144 | before do 145 | create_list(:brewery, 2, state_province: "New York") 146 | create(:brewery, state_province: "California") 147 | create(:brewery, state_province: "Delaware") 148 | create(:brewery, state_province: "dolnośląskie") 149 | end 150 | 151 | it "returns a filtered list of breweries" do 152 | get "/breweries", params: { by_state: "california" } 153 | expect(json.size).to eq(1) 154 | end 155 | 156 | it "returns a filtered list of breweries with snake case" do 157 | get "/breweries", params: { by_state: "new_york" } 158 | expect(json.size).to eq(2) 159 | end 160 | 161 | # Kebab-case doesn't seem to jive with sanitization 162 | it "returns empty list with kebab case" do 163 | get "/breweries", params: { by_state: "new-york" } 164 | expect(json.size).to eq(0) 165 | end 166 | 167 | it "returns filtered list with + as space" do 168 | get "/breweries", params: { by_state: "new+york" } 169 | expect(json.size).to eq(2) 170 | end 171 | 172 | it "returns empty list when abbreviation" do 173 | get "/breweries", params: { by_state: "ny" } 174 | expect(json.size).to eq(0) 175 | end 176 | 177 | it "returns empty list when mispelled" do 178 | get "/breweries", params: { by_state: "delwar" } 179 | expect(json.size).to eq(0) 180 | end 181 | 182 | it "returns a filtered list when utf-8" do 183 | get "/breweries", params: { by_state: "dolnośląskie" } 184 | expect(json.size).to eq(1) 185 | end 186 | 187 | it "sanitizes for SQL LIKE \\" do 188 | get "/breweries", params: { by_state: "Cali\\" } 189 | expect(response).to have_http_status(:ok) 190 | end 191 | 192 | it "sanitizes for SQL LIKE %" do 193 | get "/breweries", params: { by_state: "Cali%" } 194 | expect(response).to have_http_status(:ok) 195 | end 196 | end 197 | 198 | context "when by_type param is passed" do 199 | before do 200 | create_list(:brewery, 3, brewery_type: "planning") 201 | end 202 | 203 | it "returns a filtered list of breweries, when valid type" do 204 | get "/breweries", params: { by_type: "planning" } 205 | expect(json.size).to eq(3) 206 | end 207 | 208 | it "throws a 400 error, when invalid type" do 209 | get "/breweries", params: { by_type: "notvalid" } 210 | expect(response).to have_http_status(:bad_request) 211 | end 212 | end 213 | 214 | context "when exclude_types param is passed" do 215 | before do 216 | create_list(:brewery, 2, brewery_type: "micro") 217 | create_list(:brewery, 2, brewery_type: "nano") 218 | create_list(:brewery, 3, brewery_type: "planned") 219 | end 220 | 221 | it "returns a filtered list of breweries - single" do 222 | get "/breweries", params: { exclude_types: "micro" } 223 | expect(json.size).to eq(5) 224 | end 225 | 226 | it "returns a filtered list of breweries - multiple" do 227 | get "/breweries", params: { exclude_types: "micro,nano" } 228 | expect(json.size).to eq(3) 229 | end 230 | end 231 | 232 | context "when postal param is passed" do 233 | before do 234 | create_list(:brewery, 5) 235 | create_list(:brewery, 3, postal_code: "44107") 236 | create_list(:brewery, 2, postal_code: "44107-5555") 237 | create_list(:brewery, 1, postal_code: "WC2N 5DU") 238 | end 239 | 240 | it "returns a filtered list of breweries for US postal codes" do 241 | get "/breweries", params: { by_postal: "44107" } 242 | expect(json.size).to eq(5) 243 | end 244 | 245 | it "returns a filtered list of breweries for US postal code ZIP+4" do 246 | get "/breweries", params: { by_postal: "44107-5555" } 247 | expect(json.size).to eq(2) 248 | end 249 | 250 | it "returns a filtered list of breweries for international postal codes" do 251 | get "/breweries", params: { by_postal: "WC2N" } 252 | expect(json.size).to eq(1) 253 | end 254 | end 255 | 256 | context "when distance param is passed" do 257 | before do 258 | create_list(:brewery, 5) 259 | end 260 | 261 | it "returns 200 when valid params" do 262 | get "/breweries", params: { by_dist: "74,-114" } 263 | expect(response).to have_http_status(:ok) 264 | end 265 | 266 | it "throws a 400 error when invalid params" do 267 | get "/breweries", params: { by_dist: "1" } 268 | expect(response).to have_http_status(:bad_request) 269 | end 270 | end 271 | 272 | context "when sort param is passed" do 273 | before do 274 | create(:brewery, name: "Alesmith", brewery_type: "micro") 275 | create(:brewery, name: "Ballast Point Brewing Company", brewery_type: "regional") 276 | create(:brewery, name: "Circle 9 Brewing", brewery_type: "micro") 277 | get "/breweries", params: { sort: "type,name:desc" } 278 | end 279 | 280 | it "returns a sorted list of breweries" do 281 | expect(json.map { |brewery| brewery["name"] }).to eq( 282 | ["Circle 9 Brewing", "Alesmith", "Ballast Point Brewing Company"] 283 | ) 284 | end 285 | end 286 | end 287 | 288 | describe "GET /breweries/meta" do 289 | before do 290 | create(:brewery) 291 | create(:brewery, state_province: "Springfield") 292 | create(:brewery, state_province: "dolnośląskie") 293 | create(:brewery, country: "Poland") 294 | create(:brewery, postal_code: "OBDB123") 295 | end 296 | 297 | it "returns status code 200" do 298 | get "/breweries/meta" 299 | expect(response).to have_http_status(:ok) 300 | end 301 | 302 | it "returns meta data about all breweries" do 303 | get "/breweries/meta" 304 | expect(json).to eq({ "total" => "5", "per_page" => "50", "page" => "1" }) 305 | end 306 | 307 | it "returns meta data filtered by by_state" do 308 | get "/breweries/meta", params: { by_state: "dolnośląskie" } 309 | expect(json).to eq({ "total" => "1", "per_page" => "50", "page" => "1" }) 310 | end 311 | 312 | it "returns meta data filtered by by_country" do 313 | get "/breweries/meta", params: { by_country: "Poland" } 314 | expect(json).to eq({ "total" => "1", "per_page" => "50", "page" => "1" }) 315 | end 316 | 317 | it "returns meta data filtered by by_postal" do 318 | get "/breweries/meta", params: { by_postal: "OBDB123" } 319 | expect(json).to eq({ "total" => "1", "per_page" => "50", "page" => "1" }) 320 | end 321 | 322 | it "returns meta data with per_page" do 323 | get "/breweries/meta", params: { per_page: 2 } 324 | expect(json).to eq({ "total" => "5", "per_page" => "2", "page" => "1" }) 325 | end 326 | 327 | it "returns meta data with page" do 328 | get "/breweries/meta", params: { per_page: 2, page: 3 } 329 | expect(json).to eq({ "total" => "5", "per_page" => "2", "page" => "3" }) 330 | end 331 | end 332 | 333 | describe "GET /breweries/random" do 334 | before do 335 | create_list(:brewery, 55) 336 | end 337 | 338 | it "returns a brewery" do 339 | get "/breweries/random" 340 | expect(json.size).to eq(1) 341 | end 342 | 343 | it "returns status code 200" do 344 | get "/breweries/random" 345 | expect(response).to have_http_status(:ok) 346 | end 347 | 348 | it "returns a number of breweries when size param" do 349 | get "/breweries/random", params: { size: 3 } 350 | expect(json.size).to eq(3) 351 | end 352 | 353 | it "does not return more breweries than the max allowed" do 354 | get "/breweries/random", params: { size: 51 } 355 | expect(json.size).to eq(50) 356 | end 357 | end 358 | 359 | describe "GET /breweries/:id" do 360 | let!(:brewery) { create(:brewery) } 361 | let(:brewery_id) { brewery.id } 362 | 363 | before { get "/breweries/#{brewery_id}" } 364 | 365 | context "when the record exists" do 366 | it "returns status code 200" do 367 | expect(response).to have_http_status(:ok) 368 | end 369 | end 370 | 371 | context "when the record does not exist" do 372 | let(:brewery_id) { "fictional-brewery-nowhere" } 373 | 374 | it "returns status code 404" do 375 | expect(response).to have_http_status(:not_found) 376 | end 377 | 378 | it "returns a not found message" do 379 | expect(response.body).to match(/Couldn't find Brewery/) 380 | end 381 | end 382 | end 383 | 384 | describe "POST /breweries" do 385 | let(:valid_attributes) { { name: "Awesome Brewery" } } 386 | 387 | context "when the request is valid" do 388 | it "returns returns a routing error" do 389 | expect { post "/breweries", params: valid_attributes }.to raise_error( 390 | ActionController::RoutingError 391 | ) 392 | end 393 | end 394 | end 395 | 396 | describe "PUT /breweries/:id" do 397 | let!(:brewery) { create(:brewery) } 398 | let(:valid_attributes) { { name: "Another Brewery" } } 399 | 400 | context "when the record exists" do 401 | it "returns a routing error" do 402 | expect do 403 | put "/breweries/#{brewery.id}", params: valid_attributes 404 | end.to raise_error(ActionController::RoutingError) 405 | end 406 | end 407 | end 408 | 409 | describe "DELETE /breweries/:id" do 410 | let!(:brewery) { create(:brewery) } 411 | let(:valid_attributes) { { name: "Another Brewery" } } 412 | 413 | it "return a routing error" do 414 | expect { delete "/breweries/#{brewery.id}" }.to raise_error( 415 | ActionController::RoutingError 416 | ) 417 | end 418 | end 419 | end 420 | -------------------------------------------------------------------------------- /spec/requests/v1_breweries_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe "Breweries API" do 6 | describe "GET /v1/breweries" do 7 | context "when no params are passed" do 8 | before do 9 | create_list(:brewery, 201) 10 | get "/v1/breweries" 11 | end 12 | 13 | # NOTE: Set in /config/initializers/kaminari_config.rb 14 | it "returns the default number of breweries" do 15 | expect(json.size).to eq(50) 16 | end 17 | 18 | it "returns status code 200" do 19 | expect(response).to have_http_status(:ok) 20 | end 21 | 22 | it "returns Cache-Control headers" do 23 | expect(response.headers["Cache-Control"]).to eq( 24 | "max-age=86400, public" 25 | ) 26 | end 27 | end 28 | 29 | context "when invalid params are passed" do 30 | before do 31 | create_list(:brewery, 51) 32 | get "/v1/breweries", params: { 33 | by_state: nil, 34 | page: "invalid", 35 | sort: "*bob", 36 | id: 42, 37 | brewery: { id: 42 } 38 | } 39 | end 40 | 41 | it "returns a status of 200" do 42 | expect(response).to have_http_status(:ok) 43 | end 44 | 45 | it "returns the default number of breweries" do 46 | expect(json.size).to eq(50) 47 | end 48 | end 49 | 50 | context "when page param is passed" do 51 | before do 52 | create_list(:brewery, 101) 53 | get "/v1/breweries", params: { page: 3 } 54 | end 55 | 56 | it "returns another page of breweries" do 57 | expect(json.size).to eq(1) 58 | end 59 | end 60 | 61 | context "when per_page param is passed" do 62 | before do 63 | create_list(:brewery, 205) 64 | end 65 | 66 | it "returns a limited number breweries" do 67 | get "/v1/breweries", params: { per_page: 5 } 68 | expect(json.size).to eq(5) 69 | end 70 | 71 | # NOTE: Set in /config/initializers/kaminari_config.rb 72 | it "does not exceed the maximum number of breweries per page" do 73 | get "/v1/breweries", params: { per_page: 201 } 74 | expect(json.size).to eq(200) 75 | end 76 | end 77 | 78 | context "when by_city param is passed" do 79 | before do 80 | create_list(:brewery, 1) 81 | create_list(:brewery, 1, city: "San Diego") 82 | create_list(:brewery, 1, city: "San Francisco") 83 | create_list(:brewery, 1, city: "Houston") 84 | end 85 | 86 | it "returns a filtered list of breweries" do 87 | get "/v1/breweries", params: { by_city: "Houston" } 88 | expect(json.size).to eq(1) 89 | end 90 | 91 | it "returns a filtered list with multiple" do 92 | get "/v1/breweries", params: { by_city: "San" } 93 | expect(json.size).to eq(2) 94 | end 95 | 96 | it "returns a filtered list with + as space" do 97 | get "/v1/breweries", params: { by_city: "San+Diego" } 98 | expect(json.size).to eq(1) 99 | end 100 | end 101 | 102 | context "when by_ids param is passed" do 103 | let!(:breweries) { create_list(:brewery, 51) } 104 | 105 | it "returns the expected breweries and limits to 50" do 106 | get "/v1/breweries", params: { by_ids: [breweries.map(&:id)].join(",") } 107 | expect(json.size).to eq(50) 108 | end 109 | end 110 | 111 | context "when by_country param is passed" do 112 | before do 113 | create_list(:brewery, 1, country: "England") 114 | create_list(:brewery, 1, country: "South Korea") 115 | end 116 | 117 | it "returns a filtered list of breweries" do 118 | get "/v1/breweries", params: { by_country: "England" } 119 | expect(json.size).to eq(1) 120 | end 121 | 122 | it "handles '+' as a space in the query string" do 123 | get "/v1/breweries", params: { by_country: "South+Korea" } 124 | expect(json.size).to eq(1) 125 | end 126 | end 127 | 128 | context "when by_name param is passed" do 129 | before do 130 | create_list(:brewery, 1, name: "Broad Brook Brewing Company") 131 | create_list(:brewery, 1, name: "McHappy's Brewpub Extravaganza") 132 | end 133 | 134 | it "returns a filtered list of breweries" do 135 | get "/v1/breweries", params: { by_name: "mchappy" } 136 | expect(json.size).to eq(1) 137 | end 138 | 139 | it "handles '+' as a space in the query string" do 140 | get "/v1/breweries", params: { by_name: "broad+brook" } 141 | expect(json.size).to eq(1) 142 | end 143 | 144 | it "handles a space as a space in the query string" do 145 | get "/v1/breweries", params: { by_name: "broad brook" } 146 | expect(json.size).to eq(1) 147 | end 148 | end 149 | 150 | context "when by_state param is passed" do 151 | before do 152 | create_list(:brewery, 2, state_province: "New York") 153 | create(:brewery, state_province: "California") 154 | create(:brewery, state_province: "Delaware") 155 | create(:brewery, state_province: "dolnośląskie") 156 | end 157 | 158 | it "returns a filtered list of breweries" do 159 | get "/v1/breweries", params: { by_state: "california" } 160 | expect(json.size).to eq(1) 161 | end 162 | 163 | it "returns a filtered list of breweries with snake case" do 164 | get "/v1/breweries", params: { by_state: "new_york" } 165 | expect(json.size).to eq(2) 166 | end 167 | 168 | # Kebab-case doesn't seem to jive with sanitization 169 | it "returns empty list with kebab case" do 170 | get "/v1/breweries", params: { by_state: "new-york" } 171 | expect(json.size).to eq(0) 172 | end 173 | 174 | it "returns filtered list with + as space" do 175 | get "/v1/breweries", params: { by_state: "new+york" } 176 | expect(json.size).to eq(2) 177 | end 178 | 179 | it "returns empty list when abbreviation" do 180 | get "/v1/breweries", params: { by_state: "ny" } 181 | expect(json.size).to eq(0) 182 | end 183 | 184 | it "returns empty list when mispelled" do 185 | get "/v1/breweries", params: { by_state: "delwar" } 186 | expect(json.size).to eq(0) 187 | end 188 | 189 | it "returns a filtered list when utf-8" do 190 | get "/v1/breweries", params: { by_state: "dolnośląskie" } 191 | expect(json.size).to eq(1) 192 | end 193 | 194 | it "sanitizes for SQL LIKE \\" do 195 | get "/v1/breweries", params: { by_state: "Cali\\" } 196 | expect(response).to have_http_status(:ok) 197 | end 198 | 199 | it "sanitizes for SQL LIKE %" do 200 | get "/v1/breweries", params: { by_state: "Cali%" } 201 | expect(response).to have_http_status(:ok) 202 | end 203 | end 204 | 205 | context "when by_type param is passed" do 206 | before do 207 | create_list(:brewery, 3, brewery_type: "planning") 208 | end 209 | 210 | it "returns a filtered list of breweries, when valid type" do 211 | get "/v1/breweries", params: { by_type: "planning" } 212 | expect(json.size).to eq(3) 213 | end 214 | 215 | it "throws a 400 error, when invalid type" do 216 | get "/v1/breweries", params: { by_type: "notvalid" } 217 | expect(response).to have_http_status(:bad_request) 218 | end 219 | end 220 | 221 | context "when exclude_types param is passed" do 222 | before do 223 | create_list(:brewery, 2, brewery_type: "micro") 224 | create_list(:brewery, 2, brewery_type: "nano") 225 | create_list(:brewery, 3, brewery_type: "planned") 226 | end 227 | 228 | it "returns a filtered list of breweries - single" do 229 | get "/v1/breweries", params: { exclude_types: "micro" } 230 | expect(json.size).to eq(5) 231 | end 232 | 233 | it "returns a filtered list of breweries - multiple" do 234 | get "/v1/breweries", params: { exclude_types: "micro,nano" } 235 | expect(json.size).to eq(3) 236 | end 237 | end 238 | 239 | context "when postal param is passed" do 240 | before do 241 | create_list(:brewery, 5) 242 | create_list(:brewery, 3, postal_code: "44107") 243 | create_list(:brewery, 2, postal_code: "44107-5555") 244 | create_list(:brewery, 1, postal_code: "WC2N 5DU") 245 | end 246 | 247 | it "returns a filtered list of breweries for US postal codes" do 248 | get "/v1/breweries", params: { by_postal: "44107" } 249 | expect(json.size).to eq(5) 250 | end 251 | 252 | it "returns a filtered list of breweries for US postal code ZIP+4" do 253 | get "/v1/breweries", params: { by_postal: "44107-5555" } 254 | expect(json.size).to eq(2) 255 | end 256 | 257 | it "returns a filtered list of breweries for international postal codes" do 258 | get "/v1/breweries", params: { by_postal: "WC2N" } 259 | expect(json.size).to eq(1) 260 | end 261 | end 262 | 263 | context "when distance param is passed" do 264 | before do 265 | create_list(:brewery, 5) 266 | end 267 | 268 | it "returns 200 when valid params" do 269 | get "/v1/breweries", params: { by_dist: "74,-114" } 270 | expect(response).to have_http_status(:ok) 271 | end 272 | 273 | it "throws a 400 error when invalid params" do 274 | get "/v1/breweries", params: { by_dist: "1" } 275 | expect(response).to have_http_status(:bad_request) 276 | end 277 | end 278 | 279 | context "when sort param is passed" do 280 | before do 281 | create(:brewery, name: "Alesmith", brewery_type: "micro") 282 | create(:brewery, name: "Ballast Point Brewing Company", brewery_type: "regional") 283 | create(:brewery, name: "Circle 9 Brewing", brewery_type: "micro") 284 | get "/v1/breweries", params: { sort: "type,name:desc" } 285 | end 286 | 287 | it "returns a sorted list of breweries" do 288 | expect(json.map { |brewery| brewery["name"] }).to eq( 289 | ["Circle 9 Brewing", "Alesmith", "Ballast Point Brewing Company"] 290 | ) 291 | end 292 | end 293 | end 294 | 295 | describe "GET /v1/breweries/meta" do 296 | before do 297 | create(:brewery, state_province: "dolnośląskie") 298 | create(:brewery, country: "Poland") 299 | create(:brewery, postal_code: "OBDB123") 300 | end 301 | 302 | it "returns status code 200" do 303 | get "/v1/breweries/meta" 304 | expect(response).to have_http_status(:ok) 305 | end 306 | 307 | it "returns meta data about all breweries" do 308 | get "/v1/breweries/meta" 309 | expect(json).to eq({ "total" => "3", "per_page" => "50", "page" => "1" }) 310 | end 311 | 312 | it "returns meta data filtered by by_state" do 313 | get "/v1/breweries/meta", params: { by_state: "dolnośląskie" } 314 | expect(json).to eq({ "total" => "1", "per_page" => "50", "page" => "1" }) 315 | end 316 | 317 | it "returns meta data filtered by by_country" do 318 | get "/v1/breweries/meta", params: { by_country: "Poland" } 319 | expect(json).to eq({ "total" => "1", "per_page" => "50", "page" => "1" }) 320 | end 321 | 322 | it "returns meta data filtered by by_postal" do 323 | get "/v1/breweries/meta", params: { by_postal: "OBDB123" } 324 | expect(json).to eq({ "total" => "1", "per_page" => "50", "page" => "1" }) 325 | end 326 | 327 | it "returns meta data with per_page" do 328 | get "/v1/breweries/meta", params: { per_page: 2 } 329 | expect(json).to eq({ "total" => "3", "per_page" => "2", "page" => "1" }) 330 | end 331 | 332 | it "returns meta data with page" do 333 | get "/v1/breweries/meta", params: { per_page: 2, page: 2 } 334 | expect(json).to eq({ "total" => "3", "per_page" => "2", "page" => "2" }) 335 | end 336 | end 337 | 338 | describe "GET /v1/breweries/random" do 339 | before do 340 | create_list(:brewery, 55) 341 | end 342 | 343 | it "returns a brewery" do 344 | get "/v1/breweries/random" 345 | expect(json.size).to eq(1) 346 | end 347 | 348 | it "returns status code 200" do 349 | get "/v1/breweries/random" 350 | expect(response).to have_http_status(:ok) 351 | end 352 | 353 | it "returns a number of breweries when size param" do 354 | get "/v1/breweries/random", params: { size: 3 } 355 | expect(json.size).to eq(3) 356 | end 357 | 358 | it "does not return more breweries than the max allowed" do 359 | get "/v1/breweries/random", params: { size: 51 } 360 | expect(json.size).to eq(50) 361 | end 362 | end 363 | 364 | describe "GET /v1/breweries/:id" do 365 | let!(:brewery) { create(:brewery) } 366 | let(:brewery_id) { brewery.id } 367 | 368 | before { get "/v1/breweries/#{brewery_id}" } 369 | 370 | context "when the record exists" do 371 | it "returns status code 200" do 372 | expect(response).to have_http_status(:ok) 373 | end 374 | end 375 | 376 | context "when the record does not exist" do 377 | let(:brewery_id) { "fictional-brewery-nowhere" } 378 | 379 | it "returns status code 404" do 380 | expect(response).to have_http_status(:not_found) 381 | end 382 | 383 | it "returns a not found message" do 384 | expect(response.body).to match(/Couldn't find Brewery/) 385 | end 386 | end 387 | end 388 | 389 | describe "POST /v1/breweries" do 390 | let(:valid_attributes) { { name: "Awesome Brewery" } } 391 | 392 | context "when the request is valid" do 393 | it "returns returns a routing error" do 394 | expect { post "/v1/breweries", params: valid_attributes }.to raise_error( 395 | ActionController::RoutingError 396 | ) 397 | end 398 | end 399 | end 400 | 401 | describe "PUT /v1/breweries/:id" do 402 | let!(:brewery) { create(:brewery) } 403 | let(:valid_attributes) { { name: "Another Brewery" } } 404 | 405 | context "when the record exists" do 406 | it "returns a routing error" do 407 | expect do 408 | put "/v1/breweries/#{brewery.id}", params: valid_attributes 409 | end.to raise_error(ActionController::RoutingError) 410 | end 411 | end 412 | end 413 | 414 | describe "DELETE /v1/breweries/:id" do 415 | let!(:brewery) { create(:brewery) } 416 | let(:valid_attributes) { { name: "Another Brewery" } } 417 | 418 | it "return a routing error" do 419 | expect { delete "/v1/breweries/#{brewery.id}" }.to raise_error( 420 | ActionController::RoutingError 421 | ) 422 | end 423 | end 424 | end 425 | -------------------------------------------------------------------------------- /lib/import/brewers_association/mississippi.html: -------------------------------------------------------------------------------- 1 |

We found 16 Breweries in Mississippi

2 |
    3 |
  • 1817 Brewery
  • 100 B South Olive St
  • okolona, MS 38860 | Map
  • Phone: (662) 305-5907
  • Type: Micro
    Independent Craft Brewers SealBrewers Association Member
4 |
    Independent Craft Brewers SealBrewers Association Member
6 |
    Independent Craft Brewers SealBrewers Association Member
8 |
    BA Associate Member
10 |
    Independent Craft Brewers Seal
12 |
    Independent Craft Brewers Seal
14 |
    Independent Craft Brewers SealBrewers Association Member
16 |
    Independent Craft Brewers SealBrewers Association Member
18 |
    Independent Craft Brewers SealBrewers Association Member
20 |
    Independent Craft Brewers SealBrewers Association Member
22 |
    Independent Craft Brewers SealBrewers Association Member
24 |
    Independent Craft Brewers Seal
26 |
    Independent Craft Brewers Seal
28 |
    Independent Craft Brewers SealBrewers Association Member
30 |
    BA Associate Member
32 |
    Independent Craft Brewers Seal
34 | -------------------------------------------------------------------------------- /lib/import/brewers_association/north_dakota.html: -------------------------------------------------------------------------------- 1 |

We found 17 Breweries in North Dakota

2 |
    Independent Craft Brewers SealBrewers Association Member
4 |
    5 |
  • Black Leg Brewery
  • McKenzie, ND 58572-9649
  • Type: Planning
    BA Associate Member
6 |
    Independent Craft Brewers Seal
8 |
    Independent Craft Brewers Seal
10 |
    Independent Craft Brewers SealBrewers Association Member
12 |
    13 |
  • Drumconrath Brewing Company
  • 349 Knutson St Unit A
  • Mapleton, ND 58059-4042 | Map
  • Type: Micro
    Independent Craft Brewers SealBrewers Association Member
14 |
    16 |
      Independent Craft Brewers SealBrewers Association Member
    18 |
      Independent Craft Brewers SealBrewers Association Member
    20 |
      21 |
    • Granite City Food & Brewery (#3)
    • 1636 42nd St SW
    • Fargo, ND 58103-3324 | Map
    • Phone: (701) 293-3000
    • Type: Brewpub
    • www.gcfb.net
    • Greater than 25% ownership by Granite City Food (Corp), which is a craft brewer.
      Independent Craft Brewers SealBrewers Association Member
    22 |
      Independent Craft Brewers SealBrewers Association Member
    24 |
      Independent Craft Brewers SealBrewers Association Member
    26 |
      Independent Craft Brewers SealBrewers Association Member
    28 |
      BA Associate Member
    30 |
      Independent Craft Brewers SealBrewers Association Member
    32 |
      Independent Craft Brewers SealBrewers Association Member
    34 |
      Independent Craft Brewers SealBrewers Association Member
    36 | -------------------------------------------------------------------------------- /lib/import/brewers_association/district_of_columbia.html: -------------------------------------------------------------------------------- 1 |

    We found 20 Breweries in District of Columbia

    2 |
      Independent Craft Brewers SealBrewers Association Member
    4 |
      6 |
        7 |
      • Artesanales Cervezarte C.A.
      • Av Oeste Altos De Manzanares B 16 B Manzanares - Baruta
      • Caracas, Venezuela
      • Phone: 582124289327
      • Type: Micro
      • www.cervezarte.com
        BA Associate Member
      8 |
        Independent Craft Brewers SealBrewers Association Member
      10 |
        11 |
      • Bardo Brewpub
      • 25 Potomac Ave SE
      • Washington, DC 20003-3670 | Map
      • Phone: (762) 233-7070
      • Type: Micro
      • www.bardo.beer
        Independent Craft Brewers SealBrewers Association Member
      12 |
        Independent Craft Brewers SealBrewers Association Member
      14 |
        15 |
      • Brewery in Planning - Washington
      • Washington, DC 20009-2408
      • Type: Planning
        BA Associate Member
      16 |
        BA Associate Member
      18 |
        BA Associate Member
      20 |
        21 |
      • DC Brau Brewing Company
      • 3178 Bladensburg Rd NE Ste B
      • Washington, DC 20018-2214 | Map
      • Phone: (202) 621-8890
      • Type: Regional
      • www.dcbrau.com
        Independent Craft Brewers SealBrewers Association Member
      22 |
        23 |
      • District ChopHouse and Brewery
      • 509 7th St NW
      • Washington, DC 20004-1600 | Map
      • Phone: (202) 347-3434
      • Type: Brewpub
      • www.districtchophouse.com
      • Greater than 25% ownership by CraftWorks Brewery, which is a craft brewer.
        Independent Craft Brewers SealBrewers Association Member
      24 |
        Independent Craft Brewers SealBrewers Association Member
      26 |
        Independent Craft Brewers SealBrewers Association Member
      28 |
        29 |
      • Handsome Beer Company
      • Washington, DC
      • Type: Contract
        30 |
          Independent Craft Brewers SealBrewers Association Member
        32 |
          BA Associate Member
        34 |
          BA Associate Member
        36 |
          37 |
        • Right Proper Brewing Company
        • 920 Girard St NE
        • Washington, DC 20017-3424 | Map
        • Phone: (202) 607-2337
        • Type: Micro
        • www.rightproperbrewing.com
        • Greater than 25% ownership by Right Proper Brewing Company, which is a craft brewer.
          Independent Craft Brewers SealBrewers Association Member
        38 |
          Independent Craft Brewers SealBrewers Association Member
        40 |
          Independent Craft Brewers Seal
        42 | -------------------------------------------------------------------------------- /lib/import/brewers_association/hawaii.html: -------------------------------------------------------------------------------- 1 |

        We found 24 Breweries in Hawaii

        2 |
          Independent Craft Brewers Seal
        4 |
          Independent Craft Brewers SealBrewers Association Member
        6 |
          Independent Craft Brewers SealBrewers Association Member
        8 |
          9 |
        • Brewery In Planning - Honolulu HI
        • Kailua, HI 96734-4059
        • Type: Planning
          BA Associate Member
        10 |
          11 |
        • Brewery In Planning - Kahului, HI
        • KAHULUI, HI
        • Type: Planning
          BA Associate Member
        12 |
          13 |
        • Brewery in Planning - Kunia, HI
        • Kunia, HI 96759
        • Phone: (808) 222-5728
        • Type: Planning
          BA Associate Member
        14 |
          15 |
        • Broken Boundary Brewery
        • Honolulu, HI
        • Type: Planning
          BA Associate Member
        16 |
          17 |
        • Gordon Biersch Brewery Restaurant - Honolulu
        • 1 Aloha Tower Dr Ste 1123
        • Honolulu, HI 96813-4815 | Map
        • Phone: (808) 599-4877
        • Type: Brewpub
        • www.gordonbiersch.com/restaurant
        • Greater than 25% ownership by CraftWorks Brewery, which is a craft brewer.
          Independent Craft Brewers SealBrewers Association Member
        18 |
          Independent Craft Brewers SealBrewers Association Member
        20 |
          21 |
        • Home of the Brave Brewing Company
        • 909 Waimanu St
        • Honolulu, HI 96814-3309 | Map
        • Phone: (808) 396-8112
        • Type: Brewpub
        • www.hotbbc.com
          Independent Craft Brewers Seal
        22 |
          Independent Craft Brewers SealBrewers Association Member
        24 |
          Independent Craft Brewers SealBrewers Association Member
        26 |
          Independent Craft Brewers SealBrewers Association Member
        28 |
          Independent Craft Brewers SealBrewers Association Member
        30 |
          Independent Craft Brewers SealBrewers Association Member
        32 |
          33 |
        • Kona Brewing Co
        • 74-5612 Pawai Pl
        • Kailua Kona, HI 96740-1617 | Map
        • Phone: (808) 334-1133
        • Type: Regional
        • www.konabrewingco.com
        • Greater than 25% ownership by Anheuser-Busch InBev.
          BA Associate Member
        34 |
          35 |
        • Lanikai Brewing Co
        • 175-C Hamakua Dr
        • Kailua, HI 96734 | Map
        • Type: Micro
          Independent Craft Brewers SealBrewers Association Member
        36 |
          Independent Craft Brewers SealBrewers Association Member
        38 |
          39 |
        • Maui Brewing Co, Brewpub
        • 4405 Honoapiilani Hwy Ste 217
        • Lahaina, HI 96761-9272 | Map
        • Phone: (808) 669-3474
        • Type: Brewpub
        • www.mauibrewingco.com
        • Greater than 25% ownership by Maui Brewing Co - Production, which is a craft brewer.
          Independent Craft Brewers SealBrewers Association Member
        40 |
          Independent Craft Brewers SealBrewers Association Member
        42 |
          Independent Craft Brewers SealBrewers Association Member
        44 |
          Independent Craft Brewers SealBrewers Association Member
        46 |
          Independent Craft Brewers SealBrewers Association Member
        48 |
          49 |
        • Waikiki Brewing Company, Kaka'ako
        • 831 Queen St
        • Honolulu, HI 96813-5203 | Map
        • Type: Micro
        • Greater than 25% ownership by Waikiki Brewing Company, which is a craft brewer.
          Independent Craft Brewers SealBrewers Association Member
        --------------------------------------------------------------------------------