Skip to content

Commit

Permalink
[openware#306]: labels api
Browse files Browse the repository at this point in the history
  • Loading branch information
subaru9 authored and Louis committed Apr 11, 2018
1 parent 3b4fa07 commit e41230b
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 5 deletions.
7 changes: 6 additions & 1 deletion Gemfile
Expand Up @@ -15,6 +15,7 @@ gem 'jquery-rails', '~> 4.3'
gem 'kaminari', '~> 1.1.1'
gem 'doorkeeper', '~> 4.2.6'
gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.7.1'
gem 'sneakers', '~> 2.6'
gem 'grape-swagger', '~> 0.28'
gem 'rack-cors', '~> 1.0.2', require: 'rack/cors'
Expand All @@ -41,10 +42,14 @@ group :development, :test do
end

group :test do
gem 'rspec-rails', '~> 3.7'
gem 'rspec-rails', '~> 3.7.1'
gem 'factory_bot_rails', '~> 4.8'
gem 'capybara', '~> 2.17'
gem 'selenium-webdriver', '~> 3.8'
gem 'chromedriver-helper', '~> 1.1'
gem 'shoulda-matchers', '~> 3.1.2'
end

group :development do
gem 'grape_on_rails_routes', '~> 0.3.2'
end
13 changes: 10 additions & 3 deletions Gemfile.lock
Expand Up @@ -154,11 +154,16 @@ GEM
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-swagger (0.28.0)
grape (>= 0.16.2)
grape_on_rails_routes (0.3.2)
rails (>= 3.1.1)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (0.9.1)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
i18n_data (0.8.0)
ice_nine (0.11.2)
Expand Down Expand Up @@ -281,7 +286,7 @@ GEM
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.0)
rspec-support (3.7.1)
ruby_dep (1.5.0)
rubyzip (1.2.1)
sass (3.5.5)
Expand Down Expand Up @@ -371,7 +376,9 @@ DEPENDENCIES
fog-google (~> 0.1.0)
fontello_rails_converter
grape (~> 1.0)
grape-entity (~> 0.7.1)
grape-swagger (~> 0.28)
grape_on_rails_routes (~> 0.3.2)
jquery-rails (~> 4.3)
kaminari (~> 1.1.1)
listen (~> 3.1)
Expand All @@ -383,7 +390,7 @@ DEPENDENCIES
puma (~> 3.7)
rack-cors (~> 1.0.2)
rails (~> 5.1.4)
rspec-rails (~> 3.7)
rspec-rails (~> 3.7.1)
sassc-rails (~> 1.3)
selenium-webdriver (~> 3.8)
shoulda-matchers (~> 3.1.2)
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/api/entities/label.rb
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module API
module Entities
class Label < Grape::Entity
format_with(:iso_timestamp, &:iso8601)

expose :key, documentation: { type: 'String', desc: 'Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.' }
expose :value, documentation: { type: 'String', desc: 'Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.' }
expose :scope, documentation: { type: 'String', desc: "Label scope: 'public' or 'private'" }

with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
1 change: 1 addition & 0 deletions app/controllers/api/v1/base.rb
Expand Up @@ -11,6 +11,7 @@ class Base < Grape::API
mount API::V1::Security
mount API::V1::Documents
mount API::V1::Session
mount API::V1::Labels

add_swagger_documentation base_path: '/api',
info: {
Expand Down
86 changes: 86 additions & 0 deletions app/controllers/api/v1/labels.rb
@@ -0,0 +1,86 @@
# frozen_string_literal: true

require 'doorkeeper/grape/helpers'

module API
module V1
# Responsible for CRUD for labes
class Labels < Grape::API
helpers Doorkeeper::Grape::Helpers

before { doorkeeper_authorize! }

before do
def current_account
Account.find(doorkeeper_token.resource_owner_id)
end
end

resource :labels do
desc 'List all labels for current account.'
get do
present current_account.labels, with: API::Entities::Label
end

desc 'Return a label by key.'
params do
requires :key, type: String, allow_blank: false, desc: 'Label key.'
end
route_param :key do
get do
label = current_account.labels.find_by(key: params[:key])
return error!('Couldn\'t find Label', 404) if label.blank?

present label, with: API::Entities::Label
end
end

desc "Create a label with 'public' scope."
params do
requires :key, type: String, allow_blank: false, desc: 'Label key.'
requires :value, type: String, allow_blank: false, desc: 'Label value.'
end
post do
label =
current_account.labels.new(
key: params[:key],
value: params[:value],
scope: 'public'
)
if label.save
present label, with: API::Entities::Label
else
error!(label.errors.as_json(full_messages: true), 422)
end
end

desc "Update a label with 'public' scope."
params do
requires :key, type: String, allow_blank: false, desc: 'Label key.'
requires :value, type: String, allow_blank: false, desc: 'Label value.'
end
patch ':key' do
label = current_account.labels.find_by(key: params[:key])
return error!('Couldn\'t find Label', 404) if label.blank?
return error!('Can\'t update Label.', 400) if label.private?

label.update(value: params[:value])
present label, with: API::Entities::Label
end

desc "Delete a label with 'public' scope."
params do
requires :key, type: String, allow_blank: false, desc: 'Label key.'
end
delete ':key' do
label = current_account.labels.find_by(key: params[:key])
return error!('Couldn\'t find Label', 404) if label.blank?
return error!('Can\'t update Label.', 400) if label.private?

label.destroy
status 204
end
end
end
end
end
1 change: 1 addition & 0 deletions app/models/account.rb
Expand Up @@ -13,6 +13,7 @@ class Account < ApplicationRecord
has_one :profile, dependent: :destroy
has_many :phones, dependent: :destroy
has_many :documents, dependent: :destroy
has_many :labels

before_validation :assign_uid

Expand Down
52 changes: 52 additions & 0 deletions app/models/label.rb
@@ -0,0 +1,52 @@
# frozen_string_literal: true

# Resposible for storing configurations
class Label < ApplicationRecord
belongs_to :account

SCOPES =
HashWithIndifferentAccess.new(
public: 'public', private: 'private'
)

SCOPES.keys.each do |name|
define_method "#{name}?" do
scope == SCOPES[name]
end
end

validates :scope,
inclusion: { in: SCOPES.keys }

validates :key,
length: 3..255,
format: { with: /\A[a-z0-9_-]+\z/ },
uniqueness: { scope: :account_id }

validates :value,
length: 3..255,
format: { with: /\A[A-Za-z0-9_-]+\z/ }
end

# == Schema Information
# Schema version: 20180402133658
#
# Table name: labels
#
# id :integer not null, primary key
# account_id :integer
# key :string(255) not null
# value :string(255) not null
# scope :string(255) default("public"), not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_labels_on_account_id (account_id)
# index_labels_on_key_and_value_and_account_id (key,value,account_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id) ON DELETE => cascade
#
16 changes: 16 additions & 0 deletions db/migrate/20180402133658_create_labels.rb
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class CreateLabels < ActiveRecord::Migration[5.1]
def change
create_table :labels do |t|
t.references :account
t.string :key, null: false
t.string :value, null: false
t.string :scope, null: false, default: 'public'

t.timestamps
end
add_index :labels, %i[key value account_id], unique: true
add_foreign_key :labels, :accounts, on_delete: :cascade
end
end
14 changes: 13 additions & 1 deletion db/schema.rb
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20180402122730) do
ActiveRecord::Schema.define(version: 20180402133658) do

create_table "accounts", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "uid", null: false
Expand Down Expand Up @@ -54,6 +54,17 @@
t.index ["account_id"], name: "index_documents_on_account_id"
end

create_table "labels", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.bigint "account_id"
t.string "key", null: false
t.string "value", null: false
t.string "scope", default: "public", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_labels_on_account_id"
t.index ["key", "value", "account_id"], name: "index_labels_on_key_and_value_and_account_id", unique: true
end

create_table "oauth_access_grants", id: :integer, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
Expand Down Expand Up @@ -135,6 +146,7 @@
end

add_foreign_key "documents", "accounts"
add_foreign_key "labels", "accounts", on_delete: :cascade
add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id"
add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
add_foreign_key "profiles", "accounts"
Expand Down
74 changes: 74 additions & 0 deletions spec/api/labels_spec.rb
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require 'spec_helper'

describe 'Labels API.' do
include_context 'doorkeeper authentication'

let!(:label) { create :label, account: current_account }

let(:post_params) do
{
key: ::Faker::Internet.slug(nil, '-'),
value: ::Faker::Internet.slug(nil, '-')
}
end

context 'Happy paths for the current account.' do
it 'Return lables for current account' do
get '/api/v1/labels', headers: auth_header
expect(response.status).to eq(200)
expect(json_body.size).to eq(1)
expect(json_body.first[:key]).to eq(label.key)
end

it 'Return a label by key' do
get "/api/v1/labels/#{label.key}", headers: auth_header
expect(response.status).to eq(200)
expect(json_body[:key]).to eq(label.key)
end

it 'Create a label' do
post '/api/v1/labels', params: post_params, headers: auth_header
expect(response.status).to eq(201)
persisted = Label.find_by(key: json_body[:key])
expect(persisted.key).to eq(post_params[:key])
expect(persisted.value).to eq(post_params[:value])
expect(persisted.scope).to eq('public')
expect(persisted.account_id).to eq(current_account.id)
end

it 'Update a label' do
patch "/api/v1/labels/#{label.key}", params: post_params, headers: auth_header
expect(response.status).to eq(200)
persisted = Label.find_by(key: label.key)
expect(persisted.value).to eq(post_params[:value])
expect(persisted.scope).to eq('public')
end

it 'Delete a label' do
delete "/api/v1/labels/#{label.key}", headers: auth_header
expect(response.status).to eq(204)
expect(Label.find_by(key: label.key)).to be_nil
end
end

context 'Errors.' do
it 'Respond with errors on create if existing key is used for current account' do
post '/api/v1/labels', params: post_params.merge(key: label.key), headers: auth_header
expect(response.status).to eq(422)
expect(response.body).to be_include('Key has already been taken')
end

it 'Respond with error if attempted to update a private label' do
label.update(scope: 'private')
patch "/api/v1/labels/#{label.key}", params: post_params, headers: auth_header
expect(response.status).to eq(400)
end

it 'Respond with error if key not found' do
get '/api/v1/labels/blah', headers: auth_header
expect(response.status).to eq(404)
end
end
end
10 changes: 10 additions & 0 deletions spec/factories/labels.rb
@@ -0,0 +1,10 @@
# frozen_string_literal: true

FactoryBot.define do
factory :label do
key { ::Faker::Internet.slug(nil, '-') }
value { ::Faker::Internet.slug(nil, '-') }
scope 'public'
account { create(:account) }
end
end

0 comments on commit e41230b

Please sign in to comment.