<file_sep>DataPostTimeSchema = Dry::Validation.Form(ApplicationSchema) do
  DATE_REGEX = /\A([0-2][\d]):([0-5][\d])\s([0-3][\d])\.([0-1][\d])\z/

  configure do
    def is_date?(value)
      !DATE_REGEX.match(value).nil?
    end

    def auto_date?(value)
      %w(end beginning).any? { |d| d == value }
    end
  end

  required(:value) { filled? & (is_date? | auto_date?) }
end
<file_sep>GC::Profiler.enable if Rails.env.production?
<file_sep>RSpec.configure do |config|
  config.include ActiveJob::TestHelper, type: :job
  ActiveJob::Base.queue_adapter = :test
end
<file_sep>require 'rails_helper'

RSpec.describe TimeData, type: :model do
  subject { FactoryGirl.build(:time_data) }
  it { should validate_presence_of(:value) }
  it { should validate_uniqueness_of(:value) }
end
<file_sep>RSpec.configure do |config|
  config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/
  config.include ActiveModelSerializers::Test::Schema, file_path: /spec\/controllers\/api/
end
<file_sep>module Constraints
  class ApiConstraint
    ACCEPT_BASE = 'application/vnd.apisvc.v'.freeze

    attr_reader :version
    attr_reader :default

    def initialize(options)
      @version = options.fetch(:version)
      @default = options.fetch(:default)
    end

    def matches?(request)
      acc = accept(request)
      if acc&.include?(ACCEPT_BASE)
        acc.include?("#{ACCEPT_BASE}#{@version}")
      else
        default
      end
    end

    private

    def accept(request)
      request.headers['Accept']
    end
  end
end
<file_sep>require 'rails_helper'

RSpec.describe TimeHelpers do
  describe '#prepare_time_value' do
    it 'returns with end of day' do
      date = described_class.prepare_time_value('end')
      expect(date.zone).to eq('UTC')
      expect(date.hour).to eq(23)
      expect(date.min).to eq(59)
    end

    it 'returns with beginning of the day' do
      date = described_class.prepare_time_value('beginning')
      expect(date.zone).to eq('UTC')
      expect(date.hour).to eq(0)
      expect(date.min).to eq(0)
    end

    it 'returns parsed date' do
      date = described_class.prepare_time_value('10:33 11.11')
      expect(date.zone).to eq('UTC')
      expect(date.hour).to eq(10)
      expect(date.min).to eq(33)
      expect(date.day).to eq(11)
      expect(date.month).to eq(11)
    end

    it 'returns nil' do
      date = described_class.prepare_time_value('44:33 11.11')
      expect(date).to be_nil
    end
  end

  describe '#time_zone_by_offset' do
    it 'returns Moscow' do
      expect(described_class.time_zone_by_offset(3)).to eq('Europe/Athens')
    end

    it 'returns UTC' do
      expect(described_class.time_zone_by_offset(0)).to eq('Atlantic/Azores')
    end

    it 'returns UTC' do
      expect(described_class.time_zone_by_offset(5)).to eq('Asia/Yekaterinburg')
    end
  end
end
<file_sep>require 'shoulda-matchers'

module ShouldaMatchersFixDeprecation
  def matches?(controller)
    @controller = controller
    ensure_action_and_verb_present!

    parameters_double_registry.register

    Doublespeak.with_doubles_activated do
      context.__send__(verb, action, params: request_params)
    end

    unpermitted_parameter_names.empty?
  end
end

Shoulda::Matchers::ActionController::PermitMatcher.send(:include, ShouldaMatchersFixDeprecation)

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec

    # Choose one or more libraries:
    # with.library :active_record
    # with.library :active_model
    # with.library :action_controller
    # Or, choose the following (which implies all of the above):
    with.library :rails
  end
end
<file_sep>ifndef SERVER
	SERVER = production
endif

migrate:
	bundle exec rails db:create db:migrate

seed:
	bundle exec rails db:seed

doc_v1:
	rails api:v1:schema
<file_sep>class TimeDataSerializer < ApplicationSerializer
  cache key: 'time_data', expires_in: 3.hours
  attributes :value
end
<file_sep>require 'rails_helper'

RSpec.describe 'Data V1', type: :request do
  describe 'v1 GET /data' do
    it 'returns http success' do
      build(:time_data)
      get '/data', params: {}, headers: { 'accept' => 'application/vnd.apisvc.v1' }, as: :json
      expect(response).to have_http_status(200)
    end

    it 'fallback by default' do
      build(:time_data)
      get '/data', params: {}, as: :json
      expect(response).to have_http_status(200)
    end
  end

  describe 'v2 GET /data' do
    it 'returns 404' do
      build(:time_data)
      expect do
        get '/data', params: {}, headers: { 'accept' => 'application/vnd.apisvc.v2' }, as: :json
      end.to raise_exception(ActionController::RoutingError)
    end
  end

  describe 'v0 GET /data' do
    it 'returns 404' do
      build(:time_data)
      expect do
        get '/data', params: {}, headers: { 'accept' => 'application/vnd.apisvc.v0' }, as: :json
      end.to raise_exception(ActionController::RoutingError)
    end
  end
end
<file_sep>require 'rails_helper'

RSpec.describe LocationUpdateJob, type: :job do
  let(:valid_location_attributes) do
    { type: 'location', name: 'eee', latitude: 55.000, longitude: 44.0000, timezone: '-03:00' }
  end

  let(:invalid_location_attributes) do
    { name: 'eee', latitude: 'dfdd', longitude: 'dfdd', timezone: 444 }
  end

  it 'with enqueued job' do
    expect do
      LocationUpdateJob.perform_later(DataPostLocationSchema.call(valid_location_attributes).output)
    end.to have_enqueued_job
  end

  it 'with success job' do
    expect do
      LocationUpdateJob.perform_now(DataPostLocationSchema.call(valid_location_attributes).output)
    end.to change(LocationData, :count).by(1)
  end

  it 'with failed job' do
    expect do
      LocationUpdateJob.perform_now(DataPostLocationSchema.call(invalid_location_attributes).output)
    end.not_to change(LocationData, :count)
  end
end
<file_sep>DataPostSchema = Dry::Validation.Form(ApplicationSchema) do
  configure do
    def location_or_time?(value)
      %w(location time).any? { |lt| lt == value }
    end
  end

  required(:type) { filled? & str? & location_or_time? }
end
<file_sep>## ApiSvc Ruby Backend

### Написать сервис с API.
Сервис должен иметь один endpoint и предоставлять возможность версионирования (версия указывается в header'e запроса).
Url endpoint'a - '/data'.

### Версионирование

Передача версии осуществляется при помощи заголовка `Accept`, например:

`application/vnd.apisvc.v1`

В случае отсутствия заголовка используется версия по-умолчанию.

### Правила

Метод           | Путь              | Тип (параметр `type`) | Обязательные параметры | Доп. параметры |
:----------------|:------------------|:-------| :-------------------- | :----------- |
`POST` | `/data` | location | latitude `double`, longitude `double`, name `string` | timezone `-04:00` |
`POST` | `/data` | time | value `15:30 11.09` UTC, `end`, `beginning` | - |
`GET` | `/data` | - | - | timezone `string` |

### Setup

        # Install gems
        $ bundle install

        # Setup DB
        $ make migrate

        # Seed into DB
        $ make seed
        
        # Run project!
        $ bin/rails s     
                    
### Dependencies:

- Ruby 2.3+
- Rails 5.0
- PostgreSQL
- Memcached
- Puma

#### Few words about:
- This project is a little API app.

#### Post запрос:
Обязательный параметр - type.  
Может принимать два значения: 
- 'time'
- 'location'.

##### Если параметр 'time', 
обязательный параметр: 

- 'value' (datetime)
    который может принимать значения в формате:
    - 'hour:minutes day.month' (19:25 13.11)
    - 'end' (конец текущего дня 23:59)
    - 'beginning' (начало текущего дня 00:00)

##### В случае 'location', 
обязательны три параметра: 

- 'latitude' (double, 4 зн после точки)
- 'longitude' (double)
- 'name' (string) 
- необязательный 'timezone' (вида '-04:00')

Полученные данные сохраняются в бд.
Время отдельно, локации отдельно.

#### Get запрос:
Единственный query параметр 'timezone' должен указывать на 'name' одного из хранимых 'location'.
Endpoint должен возвращать список хранимых 'time' для указанной timezone.
В случае отсутствия query параметра, либо пустого поля 'timezone' для указанного 'location', возвращать в utc.

Использовать JSON API формат ответов. Добавить ошибки по своему усмотрению. Покрыть код тестами. Указать используемую database и web server.
Сервис должен быть рассчитан на средне-высокую нагрузку.

Copyright (c) 2016 <NAME>

MIT License
<file_sep>module ErrorHandler
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordNotFound, with: :error
    rescue_from Exception, with: :error
    rescue_from ActionController::RoutingError, with: :error
  end

  def raise_not_found
    raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
  end

  private

  def error(e)
    return if performed?
    fetch_exception(e)
    error_from_response(e)
  end

  def error_from_response(e)
    self.response_body = nil
    message = Rails.env.development? || Rails.env.test? ? "#{e&.class.name} : #{e&.message}" : e&.message
    error_info = {
      error: @rescue_response,
      message: message
    }
    unless Rails.env.production?
      Rails.logger.error e
      e&.backtrace[0, 10].each { |l| Rails.logger.error l }
    end
    error_info[:trace] = e&.backtrace[0, 10] if Rails.env.development?
    render json: error_info.to_json, status: @status_code
  end

  def fetch_exception(e)
    @status_code = ActionDispatch::ExceptionWrapper.new(request.env, e).status_code
    @rescue_response = ActionDispatch::ExceptionWrapper.rescue_responses[e&.class.name]
  end
end
<file_sep>ActiveModelSerializers.config.adapter = :json_api
ActiveModelSerializers.config.schema_path = 'spec/support/schemas'
<file_sep>source 'https://rubygems.org'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '5.0.0.1'
# Use postgresql as the database for Active Record
gem 'pg', '~> 0.18'
# Use Puma as the app server
gem 'puma'

# API
gem 'active_model_serializers', github: 'rails-api/active_model_serializers'

# gem 'foreman', require: false

# Validate
gem 'dry-validation', '~> 0.10.0'

# JSON
gem 'oj'
gem 'oj_mimic_json'

# Logs
gem 'lograge'

# Protection
gem 'rack-protection'
gem 'rack-attack'
gem 'rack-cors'
gem 'rack-timeout', require: 'rack/timeout/base'

# Cache
gem 'dalli'

group :development, :test do
  gem 'ruby-debug-ide'
  gem 'debase'

  gem 'factory_girl_rails', '~> 4.5'

  gem 'rubocop', require: false
  gem 'brakeman', require: false

  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  # gem 'byebug'

  # Use Capistrano for deployment
  # gem 'capistrano-rails', group: :development

  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-commands-rspec'
  gem 'coveralls'

  gem 'rspec-rails', '~> 3.5.0'
end

gem 'json_schema'

group :development do
  gem 'listen', '~> 3.0.5'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :test do
  gem 'database_cleaner'

  gem 'shoulda-matchers', '3.1.1', require: false
  gem 'rails-controller-testing', require: false
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
<file_sep>class LocationData < ApplicationRecord
  validates :name, uniqueness: true, presence: true
  validates :latitude, presence: true, numericality: true
  validates :longitude, presence: true, numericality: true
end
<file_sep>--
-- PostgreSQL database dump
--

-- Dumped from database version 9.5.4
-- Dumped by pg_dump version 9.5.4

SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;


--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: -
--

COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';


SET search_path = public, pg_catalog;

SET default_tablespace = '';

SET default_with_oids = false;

--
-- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE ar_internal_metadata (
    key character varying NOT NULL,
    value character varying,
    created_at timestamp without time zone NOT NULL,
    updated_at timestamp without time zone NOT NULL
);


--
-- Name: location_data; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE location_data (
    id integer NOT NULL,
    latitude numeric(30,4) NOT NULL,
    longitude numeric(30,10) NOT NULL,
    name character varying DEFAULT ''::character varying NOT NULL,
    timezone character varying DEFAULT 'Etc/UTC'::character varying
);


--
-- Name: location_data_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE location_data_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: location_data_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE location_data_id_seq OWNED BY location_data.id;


--
-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE schema_migrations (
    version character varying NOT NULL
);


--
-- Name: time_data; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE time_data (
    id integer NOT NULL,
    value timestamp without time zone NOT NULL
);


--
-- Name: time_data_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE time_data_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: time_data_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE time_data_id_seq OWNED BY time_data.id;


--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY location_data ALTER COLUMN id SET DEFAULT nextval('location_data_id_seq'::regclass);


--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY time_data ALTER COLUMN id SET DEFAULT nextval('time_data_id_seq'::regclass);


--
-- Name: ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY ar_internal_metadata
    ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);


--
-- Name: location_data_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY location_data
    ADD CONSTRAINT location_data_pkey PRIMARY KEY (id);


--
-- Name: schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY schema_migrations
    ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);


--
-- Name: time_data_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY time_data
    ADD CONSTRAINT time_data_pkey PRIMARY KEY (id);


--
-- Name: index_location_data_on_name; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX index_location_data_on_name ON location_data USING btree (name);


--
-- Name: index_time_data_on_value; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX index_time_data_on_value ON time_data USING btree (value);


--
-- PostgreSQL database dump complete
--

SET search_path TO "$user", public;

INSERT INTO schema_migrations (version) VALUES ('20160921191155'), ('20160921191157');


<file_sep>DataGetSchema = Dry::Validation.Form(ApplicationSchema) do
  optional(:timezone).maybe(:str?)
end
<file_sep>class WelcomeController < ActionController::API
  def index
    render body: 'Welcome to the roots!'
  end
end
<file_sep>DataPostLocationSchema = Dry::Validation.Form(ApplicationSchema) do
  required(:latitude).filled(:float?)
  required(:longitude).filled(:float?)
  required(:name).maybe(:str?)
  optional(:timezone).maybe(:str?)
end
<file_sep>class TimeData < ApplicationRecord
  validates :value, uniqueness: true, presence: true
end
<file_sep>Rails.application.config.middleware.insert 0, Rack::Attack

if Rails.env.production?
  Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout
  Rack::Timeout.timeout = 10 # seconds
end

Rails.application.config.middleware.insert_after Rack::Cors, Rack::Protection,
                                                 except: [:remote_token, :session_hijacking]
<file_sep>module TimeHelpers
  def self.prepare_time_value(value)
    case value
    when 'end'
      Time.zone.now.end_of_day
    when 'beginning'
      Time.zone.now.beginning_of_day
    else
      begin
        Time.zone.strptime(value, '%H:%M %d.%m')
      rescue => _e
        nil
      end
    end
  end

  def self.time_zone_by_offset(offset)
    ActiveSupport::TimeZone.all.detect do |zone|
      t = Time.current.in_time_zone(zone)
      (t.utc_offset / 3600) == offset
    end&.tzinfo&.name || 'Etc/UTC'
  end
end
<file_sep>TimeData.create((1..20).map { |_| { value: Time.current.utc } })
LocationData.create(latitude: 55.0000, longitude: 44.0000, name: 'test', timezone: 'Asia/Yekaterinburg')
<file_sep>require 'rails_helper'

RSpec.describe Api::V1::DataController, type: :controller do
  include_context 'api_v1'
  include ActiveJob::TestHelper

  let(:valid_post_attributes) do
    { type: 'location' }
  end

  let(:invalid_post_attributes) do
    { type: 'eee' }
  end

  let(:valid_location_attributes) do
    { type: 'location', name: 'eee', latitude: 55.000, longitude: 44.0000, timezone: '-03:00' }
  end

  let(:valid_time_attributes) do
    { type: 'time', value: '19:24 11.11' }
  end

  let(:valid_time_end_attributes) do
    { type: 'time', value: 'end' }
  end

  let(:valid_time_beginning_attributes) do
    { type: 'time', value: 'beginning' }
  end

  let(:invalid_time_attributes) do
    { type: 'time', value: '19:24 11.aa' }
  end

  let(:invalid_location_attributes) do
    { type: 'location', name: 'eee', latitude: 'dfdd', longitude: 'dfdd', timezone: 444 }
  end

  let(:valid_attributes) do
    { timezone: 'some' }
  end

  let(:invalid_attributes) do
    { timezone: 444 }
  end

  let(:valid_session) { {} }

  describe 'GET #index' do
    it 'assigns all times as @times within timezone' do
      create(:location_data, name: 'some', timezone: 'Europe/Moscow')
      time_data = create(:time_data)
      get :index, params: valid_attributes, session: valid_session
      expect(response).to have_http_status(:success)
      expect(assigns(:times)).to eq([time_data])
      expect(assigns(:location)).to be_a(LocationData)
      assert_response_schema('data/get.json')
      expect(JSON.parse(response.body)['data'][0]['attributes']['value']).to include('.000+03:00')
    end

    it 'assigns all times as @times with utc' do
      time_data = create(:time_data)
      get :index, params: valid_attributes, session: valid_session
      expect(response).to have_http_status(:success)
      expect(assigns(:times)).to eq([time_data])
      expect(assigns(:location)).to be_nil
      assert_response_schema('data/get.json')
      expect(JSON.parse(response.body)['data'][0]['attributes']['value']).to include('.000Z')
    end

    it 'returns empty array' do
      get :index, params: invalid_attributes, session: valid_session
      expect(response).to have_http_status(:success)
      expect(assigns(:times)).to eq([])
      assert_response_schema('data/get.json')
    end
  end

  describe 'POST #create' do
    context 'time' do
      context 'with valid params' do
        it 'creates a new TimeData' do
          expect do
            post :create, params: valid_time_attributes, session: valid_session
          end.to change(TimeData, :count).by(1)
          expect(response).to have_http_status(200)
          assert_response_schema('data/post.json')
        end

        it 'creates a new TimeData for end of the day' do
          expect do
            post :create, params: valid_time_end_attributes, session: valid_session
          end.to change(TimeData, :count).by(1)
          expect(response).to have_http_status(200)
          assert_response_schema('data/post.json')
        end

        it 'creates a new TimeData for beginning of the day' do
          expect do
            post :create, params: valid_time_beginning_attributes, session: valid_session
          end.to change(TimeData, :count).by(1)
          expect(response).to have_http_status(200)
          assert_response_schema('data/post.json')
        end

        it 'assigns a newly created time_data as @time_data' do
          post :create, params: valid_time_attributes, session: valid_session
          expect(response).to have_http_status(200)
          expect(assigns(:time_data)).to be_a(TimeData)
          expect(assigns(:time_data)).to be_persisted
          expect(assigns(:time_data).value.time_zone.name).to eq('UTC')
          assert_response_schema('data/post.json')
        end
      end

      context 'with invalid params' do
        it 'unsaved time_data as @time_data' do
          post :create, params: invalid_time_attributes, session: valid_session
          expect(response).to have_http_status(422)
          expect(assigns(:time_data)).to be_a(TimeData)
          expect(JSON.parse(response.body)['errors'].keys).to eq(['value'])
          assert_response_schema('data/error.json')
        end

        it 'error 422 on invalid hours format' do
          post :create, params: { type: 'time', value: '55:24 11.11' }, session: valid_session
          expect(response).to have_http_status(422)
          expect(assigns(:time_data)).to be_a(TimeData)
          expect(JSON.parse(response.body)['errors'].keys).to eq(['value'])
          assert_response_schema('data/error.json')
        end

        it 'error 422 on invalid month format' do
          post :create, params: { type: 'time', value: '15:24 11.15' }, session: valid_session
          expect(response).to have_http_status(422)
          expect(assigns(:time_data)).to be_a(TimeData)
          expect(JSON.parse(response.body)['errors'].keys).to eq(['value'])
          assert_response_schema('data/error.json')
        end

        it 'error 422 on valid but wrong time format' do
          post :create, params: { type: 'time', value: '25:24 11.11' }, session: valid_session
          expect(response).to have_http_status(422)
          expect(assigns(:time_data)).to be_a(TimeData)
          expect(JSON.parse(response.body)['errors'].keys).to eq(['value'])
          assert_response_schema('data/error.json')
        end
      end
    end
    context 'location' do
      context 'with valid params' do
        it 'creates a new LocationData' do
          expect do
            post :create, params: valid_location_attributes, session: valid_session
          end.to have_enqueued_job(LocationUpdateJob)
          expect(response).to have_http_status(200)
          assert_response_schema('data/post.json')
        end
      end

      context 'with invalid params' do
        it 'unsaved time_data as @time_data' do
          post :create, params: invalid_location_attributes, session: valid_session
          expect(response).to have_http_status(422)
          expect(JSON.parse(response.body)['errors'].keys).to eq(%w(latitude longitude))
          assert_response_schema('data/error.json')
        end
      end
    end
    context 'with valid type params' do
      it 'creates a new TimeData' do
        post :create, params: valid_post_attributes, session: valid_session
        expect(response).to have_http_status(422)
        assert_response_schema('data/error.json')
        expect(JSON.parse(response.body)['errors'].keys).not_to include('type')
      end
    end
    context 'with invalid type params' do
      it 'assigns a newly created but unsaved post as @post' do
        post :create, params: invalid_post_attributes, session: valid_session
        expect(response).to have_http_status(422)
        expect(JSON.parse(response.body)['errors'].keys).to include('type')
        assert_response_schema('data/error.json')
      end
    end
  end
end
<file_sep>require 'spec_helper'

RSpec.shared_context 'api_v1' do
  # let(:schema_url_prefix) { '/data' }

  before :each do
    request.headers.merge!('Accept' => 'application/vnd.apisvc.v1')
  end
end
<file_sep>class LocationUpdateJob < ApplicationJob
  queue_as :default

  def perform(data)
    LocationData.where(name: data[:name]).first_or_initialize.tap do |location|
      data[:timezone] = TimeHelpers.time_zone_by_offset(data[:timezone].to_i)
      location.attributes = data
      location.save
    end
  end
end
<file_sep>RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:example) do
    DatabaseCleaner.strategy = if RSpec.current_example.metadata[:js]
                                 :truncation
                               else
                                 :transaction
                               end
    ActiveRecord::Base.connection.increment_open_transactions if ActiveRecord::Base.connection.open_transactions < 0
    DatabaseCleaner.start unless RSpec.current_example.metadata[:nodb]
  end

  config.before(:example, type: :feature) do
    # :rack_test driver's Rack app under test shares database connection
    # with the specs, so we can use transaction strategy for speed.
    driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

    DatabaseCleaner.strategy = if driver_shares_db_connection_with_specs
                                 :transaction
                               else
                                 # Non-:rack_test driver is probably a driver for a JavaScript browser
                                 # with a Rack app under test that does *not* share a database
                                 # connection with the specs, so we must use truncation strategy.
                                 :truncation
                               end
  end

  config.after(:example) do
    DatabaseCleaner.clean unless RSpec.current_example.metadata[:nodb]
  end
end
<file_sep>class CreateTableTimeData < ActiveRecord::Migration[5.0]
  def up
    create_table :time_data do |t|
      t.datetime :value, null: false
    end

    add_index :time_data, :value, unique: true
  end

  def down
    drop_table :time_data
  end
end
<file_sep>FactoryGirl.define do
  factory :time_data do
    value Time.zone.parse('2015-08-27T12:09:36Z')
  end
end
<file_sep>module Api
  module V1
    class DataController < ApplicationController
      before_action :set_location, only: [:index]

      def index
        if index_params.success?
          Time.use_zone(@location&.timezone || 'Etc/UTC') do
            @times = TimeData.all
            render json: @times
          end
        else
          render json: { errors: index_params.messages }, status: :unprocessable_entity
        end
      end

      def create
        render json: { errors: type_params.messages }, status: :unprocessable_entity unless type_params.success?

        case type_params.output[:type]
        when 'time'
          post_time
        when 'location'
          post_location
        end
      end

      private

      def post_time
        render json: { errors: time_params.messages }, status: :unprocessable_entity unless time_params.success?

        data = time_params.output
        data[:value] = TimeHelpers.prepare_time_value(data[:value])
        @time_data = TimeData.new(data)
        if @time_data.save
          render json: { success: true }, status: :ok
        else
          render json: { errors: @time_data.errors }, status: :unprocessable_entity
        end
      end

      def post_location
        if location_params.success?
          LocationUpdateJob.perform_later(location_params.output)
          render json: { success: true }, status: :ok
        else
          render json: { errors: location_params.messages }, status: :unprocessable_entity
        end
      end

      def set_location
        @location = LocationData.find_by(name: index_params.output[:timezone])
      end

      def type_params
        DataPostSchema.call(params.as_json)
      end

      def time_params
        DataPostTimeSchema.call(params.as_json)
      end

      def location_params
        DataPostLocationSchema.call(params.as_json)
      end

      def index_params
        DataGetSchema.call(params.as_json)
      end
    end
  end
end
<file_sep>require 'rails_helper'

RSpec.describe Api::ApplicationController, type: :controller do
end
<file_sep>FactoryGirl.define do
  sequence :name do |n|
    "some#{n}"
  end

  factory :location_data do
    name { generate(:name) }
    latitude 6.00000
    longitude 57.000000
    timezone 'Etc/UTC'
  end
end
<file_sep>class LocationDataSerializer < ApplicationSerializer
  cache key: 'location_data', expires_in: 3.hours
  attributes :time_zone, :name, :latitude, :longitude
end
<file_sep>class CreateTableLocationData < ActiveRecord::Migration[5.0]
  def up
    create_table :location_data do |t|
      t.decimal :latitude, precision: 30, scale: 4, null: false
      t.decimal :longitude, precision: 30, scale: 10, null: false
      t.string :name, null: false, default: ''
      t.string :timezone, default: 'Etc/UTC'
    end

    add_index :location_data, :name, unique: true
  end

  def down
    drop_table :location_data
  end
end
<file_sep>require 'rails_helper'

RSpec.describe LocationData, type: :model do
  subject { FactoryGirl.build(:location_data) }
  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:latitude) }
  it { should validate_presence_of(:longitude) }
  it { should validate_numericality_of(:longitude) }
  it { should validate_numericality_of(:latitude) }
  it { should validate_uniqueness_of(:name) }
end
<file_sep>Rails.application.routes.draw do
  root 'welcome#index'

  def data_version(version, default = false, &routes)
    api_constraint = Constraints::ApiConstraint.new(version: version, default: default)
    scope(module: "v#{version}", constraints: api_constraint, &routes)
  end

  scope :data, module: 'api', constraints: { format: 'json' } do
    data_version(1, true) do
      get '/', to: 'data#index', as: :data
      post '/', to: 'data#create'
    end
  end
end
