Tramway::Api
English Readme
coming soon...
Russian Readme
Простой в использовании, легко устанавливаемый и плохо кастомизируемый Rails-engine с готовым CRUD через API.
Принцип работы. В приложении заранее указывается для каких моделей создаётся API CRUD. Идея проекта - возможность быстрой выкатки API, с возможностью в последствии избавиться от Tramway API, когда ваш проект становится сложнее.
Гем НЕ манкипатчит стандартные классы и поведение Rails! Именно по этой причине было решено реализовать как Rails-engine, который в последствии можно просто и легко удалить из проекта.
Фичи:
- готовый CRUD для определённых разработчиком моделей
- сохранение истории изменений записей (используется гем
audited
) - поддержка загрузки файлов (используется
carrierwave
) - аутентификация пользователя через JWT (используется
knock
) - поддержка по умолчанию JSON API спецификации (через гем
active_model_serializers
) - мягкое удаление записей по умолчанию
- поддержка коммуникации по уникальному uid объектов, чтобы не публиковать ID в базе
Ограничения:
- только с ActiveRecord
- только с версией Rails 5.1.* (поддержка 5.2 вскоре будет реализована автором гема, поддержка автором Rails 6 начнётся с версии 6.1. По религиозным автор не использует Rails версий .0.
- Ruby >= 2.3
- все модели, которые будут использованы гемом должны наследоваться от
Tramway::Core::ApplicationRecord
- все модели, которые будут использованы гемом должны иметь атрибут
state
, типаstring
илиtext
. Этот атрибут нужен для реализации мягкого удаления. Полное удаление записей из базы не поддерживается - все модели, которые будут использованы гемом должны иметь атрибут
Недостатки, которые будут вскоре ликвидированы:
- ядро
tramway-core
подгружает в себя ненужных для API гемов (недостаток не имеет смысла в случае использования вместе с этим решением гемаtramway-admin
):- bootstrap
- font_awesome5_rails
- haml
- требуется ручное добавление требуемых для работы гемов
ruby gem 'active_model_serializers', '0.10.5' gem 'tramway-core' gem 'knock' gem 'audited' gem 'ransack'
Usage
rails new tramway_api_sample
Gemfile
gem 'tramway-api', '>= 1.1.0.1'
gem 'active_model_serializers', '0.10.5'
gem 'tramway-core'
gem 'state_machine', github: 'seuros/state_machine'
gem 'knock'
Run bundle install
Initialize @application object
config/routes.rb
Rails.application.routes.draw do
# ...
mount Tramway::Api::Engine, at: '/api'
# ...
end
Then generate User (you use another name, it's just an example) model
rails g model user email:text password_digest:text username:text state:text uuid:uuid
Enable extension in your database:
db/migrate/enable_extension.rb
class EnableExtensionUUIDOSSP < ActiveRecord::Migration
def change
enable_extension 'uuid-ossp'
end
end
You can choose a method, which will be using as public ID method
By default, it's a uuid
method
To choose your own public ID method, just add this line to:
config/initializers/tramway.rb
Tramway::Api.id_methods_of(User => { default: :id })
If you want to use uuid
by default, please, add it to your models
Also, you can add array of secondary methods as IDs, but you'll need to add their names to a request
Tramway::Api.id_methods_of(User => { default: id, other: :email }
in this case your request will look like this /api/v1/records/[email protected]?model=User&key=email
Add generating uuid by default to every model, that is accessible by API
db/migrate/add_uuid_to_some_model.rb
t.uuid :uuid, default: 'uuid_generate_v4()'
app/models/user.rb
class User < Tramway::Core::ApplicationRecord
has_secure_password
def self.from_token_payload(payload)
find_by uuid: payload['sub']
end
end
Create file config/initializers/tramway.rb
If you need JWT authentication add this line to the config/initializers/tramway.rb
::Tramway::Api.auth_config = { user_model: User, auth_attributes: %i[email username] }
Configurate available models. Tramway will create end points according to this config
::Tramway::Api.set_available_models({
User => [
{
show: lambda do |record, current_user|
record.id == current_user.id # shows only current_user profile
end
}
],
project: :your_project_name
})
Run rails g tramway:core:install
Run rails db:create db:migrate
config/routes.rb
Rails.application.routes.draw do
mount Tramway::Api::Engine, at: '/api'
end
Create file app/forms/user_sign_up_form.rb
class UserSignUpForm < Tramway::Core::ApplicationForm
properties :username, :email, :password
end
DONE!
Testing
Preparation (optional)
Let's write RSpec test to check what we have:
Gemfile
group :test do
gem 'rspec-rails', '~> 3.5'
gem 'rspec-json_expectations', '2.2.0'
gem 'factory_bot_rails', '~> 4.0'
gem 'json_matchers'
gem 'json_api_test_helpers', '1.1.1'
end
Run bundle install
Run RAILS_ENV=test rails db:create db:migrate
Run mkdir spec
Create file spec/spec_helper.rb
with:
ENV['RAILS_ENV'] ||= 'test'
require File.('../config/environment', __dir__)
require 'rspec/rails'
require 'rspec/autorun'
require 'rspec/expectations'
require 'rspec/json_expectations'
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
Create file spec/rails_helper.rb
with:
require 'spec_helper'
require 'factory_bot'
require 'rspec/rails'
require 'rspec/json_expectations'
require 'json_matchers/rspec'
require 'json_api_test_helpers'
require 'rake'
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.include RSpec::Rails::RequestExampleGroup, type: :feature
config.include JsonApiTestHelpers
end
SignUp user
Create file spec/tramway_api_spec.rb
with:
require 'rails_helper'
RSpec.describe 'Post creating user', type: :feature do
describe 'POST /api/v1/user with model User' do
let(:attributes) do
kebab_case_converter attributes_for :user
end
it 'returns created status' do
post '/api/v1/user', params: { user: attributes }
expect(response[:status]).to eq 201
end
it 'returns no errors' do
post '/api/v1/user', params: { user: attributes }
expect(json_response[:response]). to be_nil
end
end
end
SignIn User
require 'rails_helper'
RSpec.describe 'Post generate token', type: :feature do
describe 'POST /api/v1/user_tokens' do
let(:user) { create :user, password: '123456789' }
it 'returns created status' do
post '/api/v1/user_tokens', params: { auth: { login: user.email, password: '123456789' } }
expect(response[:status]).to eq 201
end
it 'returns token' do
post '/api/v1/user_tokens', params: { auth: { login: user.email, password: '123456789' } }
expect(json_response[:auth_token].present?).to be_truthy
expect(json_response[:user]).to include_json({ email: user.email, uuid: user.uuid })
end
end
end
Run rspec
to test
We have route user
, which create new authenticable user.
For other models we have route records
.
~: rails routes
Prefix Verb URI Pattern Controller#Action
tramway_api /api Tramway::Api::Engine
Routes for Tramway::Api::Engine:
v1_user_token POST /v1/user_token(.:format) tramway/api/v1/user_tokens#create
v1_user GET /v1/user(.:format) tramway/api/v1/users#show
POST /v1/user(.:format) tramway/api/v1/users#create
v1_records GET /v1/records(.:format) tramway/api/v1/records#index
POST /v1/records(.:format) tramway/api/v1/records#create
new_v1_record GET /v1/records/new(.:format) tramway/api/v1/records#new
edit_v1_record GET /v1/records/:id/edit(.:format) tramway/api/v1/records#edit
v1_record GET /v1/records/:id(.:format) tramway/api/v1/records#show
PATCH /v1/records/:id(.:format) tramway/api/v1/records#update
PUT /v1/records/:id(.:format) tramway/api/v1/records#update
DELETE /v1/records/:id(.:format) tramway/api/v1/records#destroy
Methods
Initializer methods
auth_config
Sets default ActiveRecord model, which used as main user to be authenticated with JWT.
user_model
- model name
auth_attributes
- array of available attributes used as login.
this model must have field password_digest
, because we use bcrypt
gem for authentication (providing other name of password attribute instead of password
is coming soon)
set_available_models
Sets ActiveRecord models which will be used in API
Enabled methods:
- create
- update
- show
- index
- destroy
Index
Every model you've added in initializer will be able by URL api/v1/records?model=#{model_class}
.
Just update your initializer:
::Tramway::Api.set_available_models({ User => { %i[index] })
Create serializer
app/serializers/user_serializer.rb
class UserSerializer < Tramway::Api::V1::ApplicationSerializer
attributes :username, :email
end
Then write test:
it 'returns status' do
get '/api/v1/records', params: { model: 'User' }, headers: headers
expect(response[:status]).to eq 200
end
it 'returns needed count' do
get '/api/v1/records', params: { model: 'User' }, headers: headers
expect(json_response[:data].size).to eq User.count
end
You have your records in JSON API spec.
You also able to use pagination, provided by kaminari
Create
config/initializers/tramway.rb
::Tramway::Api.set_available_models({ YourModel => [ :create ] }, project: :your_project_name })
app/forms/your_model_form.rb
class YourModelForm < Tramway::Core::ApplicationForm
properties :attribute1, :attribute2, :name
association :another_association_model
def name=(value)
model.first_name = value.split(' ')[0]
model.first_name = value.split(' ')[1]
end
end
Now you can your request. It has such structure
POST /api/v1/records?model=YourModel
Params Structure
{
data: {
attributes: {
attribute1: 'some value',
attribute2: 'some value',
name: 'some full name',
another_association_model: {
# here a list of attributes, which you described in AnotherAssociationModelForm
}
}
}
}
Also, you can test it with this
spec/factories/your_models.rb
FactoryBot.define do
factory :your_model do
attribute1 { # some code which generate value for this attribute }
attribute2 { # some code which generate value for this attribute }
name { generate :name }
end
end
spec/api/your_model_spec.rb
require 'rails_helper'
RSpec.describe 'Post generate token', type: :feature do
describe 'POST /api/v1/user_token' do
let(:user) { create :user, password: '123456789' }
it 'returns created status' do
post '/api/v1/user_token', params: { auth: { login: user.email, password: '123456789' } }
expect(response[:status]).to eq 201
end
it 'returns token' do
post '/api/v1/user_token', params: { auth: { login: user.email, password: '123456789' } }
expect(json_response[:auth_token].present?).to be_truthy
expect(json_response[:user]).to include_json({ email: user.email, uuid: user.uuid })
end
end
end
Update
config/initializers/tramway.rb
::Tramway::Api.set_available_models({ YourModel => [ :update ] }, project: :your_project_name })
app/forms/your_model_form.rb
class YourModelForm < Tramway::Core::ApplicationForm
properties :attribute1, :attribute2, :name
association :another_association_model
def name=(value)
model.first_name = value.split(' ')[0]
model.first_name = value.split(' ')[1]
end
end
Now you can your request. It has such structure
PATCH /api/v1/records/#{object_id}?model=YourModel
Params Structure
{
data: {
attributes: {
attribute1: 'some value',
attribute2: 'some value',
name: 'some full name',
another_association_model: {
# here a list of attributes, which you described in AnotherAssociationModelForm
}
}
}
}
Show
Description
It returns just one record, if it is not deleted.
Using
Allow method show in tramway initializer for YourModel
config/initializers/tramway.rb
::Tramway::Api.set_available_models({ YourModel => [ :show ] }, project: :your_project_name })
Create serializer
app/serializers/user_serializer.rb
class UserSerializer < Tramway::Core::ApplicationSerializer
attributes :username, :email
end
Run your server on the localhost rails s
Made this query to test new API method (for example: you can create file bin/test_tramway.rb
with this lines):
require 'net/http'
YourModel.create! attribute_1: 'some value', attribute_2: 'some_value'
Net::HTTP.get('localhost:3000', "/api/v1/records/#{YourModel.last.id}?model=YourModel")
Destroy
Production ready
Docs coming soon
Contributing
Contribution directions go here.
License
The gem is available as open source under the terms of the MIT License.