We always need rest api server with json response for client.
I try developing best practice Restful Api with rails or another framework. This tutorial use Ruby on Rails Api-only Applications.
I hope no one more suffer from many developing methods such a Unit testing with Rspec, Token base Authenticate and Authorized, Api Documentation, Storage config for upload, Log collecting .. etc
- Demo
- Feature
- Prerequisites
- How to Install and Run Tutorial rails rest api Project in your local
- Deploy on Production server
- TODO
- Tutorial Index - Rails rest api for post
- supported Unit Testing with Rspec
- supported Document with Rswag
gem 'rswag-api'
gem 'rswag-ui'
gem 'rswag-specs'
https://github.com/rswag/rswag - supported Docker-compose
- supported Heroku
- supported ELK for logs with
gem 'lograge'
- used Ruby:2.6.0 with Dockerfile
- used Rails 6
- used Active Storage for Upload file with Cloudiry free plan (Required config key)
- used
gem 'active_model_serializers'
for json response - used
gem 'jwt-rails', '~> 0.0.1'
for token based Authentication, Authorize - used
gem 'kaminari'
for pagination - used postgresql with Active record
- used redis for cache
- used https://app.snyk.io/ for security
Default storage config is Cloudinary. but i did not push master.key
you have to generate master.key and add your storage config
-
- Delete old master.key and credentials.yml.enc https://www.chrisblunt.com/rails-on-docker-rails-encrypted-secrets-with-docker/
$ rm config/master.key config/credentials.yml.enc
- create new master.key and credentials.yml.enc
- docker-compose
$ docker-compose run --rm -e EDITOR=vim web bin/rails credentials:edit
- Non docker-compose
$ EDITOR=vim web bin/rails credentials:edit
- result
Adding config/master.key to store the encryption key: c7713a458177982b0d951fd50649b674 Save this in a password manager your team can access. If you lose the key, no one, including you, can access anything encrypted with it. create config/master.key File encrypted and saved.
- docker-compose
- Delete old master.key and credentials.yml.enc https://www.chrisblunt.com/rails-on-docker-rails-encrypted-secrets-with-docker/
-
open
EDITOR=vim rails credentials:edit
ordocker-compose run --rm -e EDITOR=vim web bin/rails credentials:edit
- you can join free plan Cloudinary and get PROJECT_NAME, API_KEY, API_SECRET
cloudinary: cloud_name: PROJECT_NAME api_key: API_KEY api_secret: API_SECRET
- You can choice for run with
docker-compose
orNon docker-compose
. You shold better usedocker-compose
- download from github
git clone https://github.com/x1wins/tutorial-rails-rest-api.git
-
- Build and Run with Background demon
docker-compose up --build -d
- Update
Gemfile.lock
docker-compose run --no-deps web bundle docker-compose up --build -d
- Update
- Database Setup
docker-compose run web bundle exec rake db:test:load && \ docker-compose run web bundle exec rake db:migrate && \ docker-compose run web bundle exec rake db:seed --trace
- Another docker-compose Command for
rails
andrake
- How do I update Gemfile.lock on my Docker host?
https://stackoverflow.com/a/37927979/1399891
docker-compose run --no-deps web bundle
- How to remove images after building
https://forums.docker.com/t/how-to-remove-none-images-after-building/7050
docker rmi $(docker images -f “dangling=true” -q)
- Database Reset
docker-compose run web bundle exec rake db:reset --trace
- Log
- Development enviroment
tail -f log/development.log # if you wanna show sql log
- Production enviroment
tail -f log/production.log
- Development enviroment
-
docker-compose run --no-deps web bundle exec rspec --format documentation docker-compose run --no-deps web bundle exec rspec --format documentation spec/requests/api/v1/upload_spec.rb docker-compose run --no-deps web bundle exec rspec --format documentation spec/requests/api/v1/posts_spec.rb docker-compose run --no-deps web bundle exec rspec --format documentation spec/controllers/api/v1/posts_controller_spec.rb
- Rswag for documentation
http://localhost:3000/api-docs/index.html
docker-compose run --no-deps web bundle exec rake rswag
- rails console
docker-compose exec web bin/rails c
- routes
docker-compose run --no-deps web bundle exec rake routes
- How do I update Gemfile.lock on my Docker host?
https://stackoverflow.com/a/37927979/1399891
- Build and Run with Background demon
-
- bundle
bundle install
- postgresql run
rake docker:pg:init rake docker:pg:run
- migrate
rake db:migrate RAILS_ENV=test rake db:migrate rake db:seed
- redis run
docker run --rm --name my-redis-container -p 6379:6379 -d redis redis-server --appendonly yes redis-cli -h localhost -p 7001
- server run
rails s
- Another Command for
rails
andrake
- Database Reset
rake db:reset --trace
- Log
- Development enviroment
tail -f log/development.log # if you wanna show sql log
- Production enviroment
tail -f log/production.log
- Development enviroment
- Testing
bundle exec rspec --format documentation bundle exec rspec --format documentation spec/requests/api/v1/upload_spec.rb bundle exec rspec --format documentation spec/requests/api/v1/posts_spec.rb bundle exec rspec --format documentation spec/controllers/api/v1/posts_controller_spec.rb
- Rswag for documentation
http://localhost:3000/api-docs/index.html
rake rswag
- rails console
rails c
- routes
rake routes
- Database Reset
- bundle
- download from github
i did deploy to heroku. let's break it down with swagger UI
https://tutorial-rails-rest-api.herokuapp.com/api-docs/index.html
there will be auto addedHeroku Redis free plan add-on
,Heroku Postgresql free plan add-on
,Cloudinary free plan add-on
- Heroku
- install CLI https://devcenter.heroku.com/articles/heroku-cli#download-and-install
brew tap heroku/brew && brew install heroku
- Login heroku
https://dashboard.heroku.com/apps/YOUR_PORJECT_NAME/deploy/heroku-git
heroku login heroku git:clone -a YOUR_PORJECT_NAME cd YOUR_PORJECT_NAME
- migration
heroku rake db:migrate --app YOUR_PORJECT_NAME heroku rake db:seed --app YOUR_PORJECT_NAME
- Another cmd
- master.key
heroku config:set RAILS_MASTER_KEY=asdf1234 --app YOUR_PORJECT_NAME
- restart
heroku restart --app YOUR_PORJECT_NAME
- log
heroku logs --tail --app YOUR_PORJECT_NAME
- console with heroku
heroku run rails console --app YOUR_PORJECT_NAME
- master.key
- install CLI https://devcenter.heroku.com/articles/heroku-cli#download-and-install
- Docker compose in your server
- ssh
ssh -i ~/your.pem [email protected]
- install git, docker with yum on aws ec2 instance https://www.changwoo.org/[email protected]/2019-09-19/aws-setting-with-docker-git-cfac5c7d1b
sudo yum update -y sudo yum install docker sudo service docker start sudo usermod -a -G docker ec2-user sudo yum install git
- git clone
git clone https://github.com/x1wins/tutorial-rails-rest-api.git cd tutorial-rails-rest-api/
- change master.key
- docker-compose
- ssh
- Generate porject
rails new [Project Name] --api -T -d postgresql
- Database setting Gem https://github.com/x1wins/docker-postgres-rails
- User scaffold
- User scaffold and JWT for user authenticate Gem https://github.com/x1wins/jwt-rails
- User role http://railscasts.com/episodes/189-embedded-association?view=asciicast https://github.com/ryanb/cancan/wiki/Role-Based-Authorization
- avatar file upload
- generate uninque username https://alexcastano.com/generate-unique-usernames-for-ruby-on-rails/
- Category scaffold
- fix post.category serialize
- Post scaffold
- add title column
- Comment scaffold
- add depth
- file upload
- Model Serializer https://itnext.io/a-quickstart-guide-to-using-serializer-with-your-ruby-on-rails-api-d5052dea52c5
- Rspec https://relishapp.com/rspec/rspec-rails/docs/gettingstarted
- Swager https://github.com/rswag/rswag
- Add published condition of association https://www.rubydoc.info/gems/active_model_serializers/0.9.4
- Search in posts
- Pagination https://github.com/kaminari/kaminari
- categories#index
- posts#index
- posts#index Comments
- posts#show Comments
- Add json of pagination
- Parent Model 404 check in Nested Model
- Parent Category in Post#index 404 check
- Post rspec
- Parent Post, Category in Comment#index 404 check
- Comment rspec
- Parent Category in Post#index 404 check
- N+1
- log
- Versioning http://railscasts.com/episodes/350-rest-api-versioning?view=asciicast
- File upload to Local path with active storage
- create or add attached file
- delete
- docker-compose
- staging
- production
you can change active storage config to such a like cloud storage
S3 or GCS
in storage.ymlif you use heroku and you upload file on local path of Ephemeral Disk. Uploaded file will be gone in a few minutes because heroku hard drive is Ephemeral Disk
- Local
- Add
~/storage
path for saving uploaded filemkdir ~/storage
- Update
config.active_storage.service = :local
in development.rb, production.rb - Added local config in storage.yml
- Add
-
- https://cloudinary.com/documentation/rails_activestorage
- https://github.com/0sc/activestorage-cloudinary-service
- Added api key
- Add gemfile
gem 'cloudinary' gem 'activestorage-cloudinary-service'
- open
config/storage.yml
cloudinary: service: Cloudinary cloud_name: <%= Rails.application.credentials.dig(:cloudinary, :cloud_name) %> api_key: <%= Rails.application.credentials.dig(:cloudinary, :api_key) %> api_secret: <%= Rails.application.credentials.dig(:cloudinary, :api_secret) %>
- Changing master.key
- Clodinary config
- Add gemfile
class ApplicationController < ActionController::API
def authorize_request
header = request.headers['Authorization']
header = header.split(' ').last if header
begin
@decoded = JsonWebToken.decode(header)
@current_user = User.find(@decoded[:user_id])
is_banned @current_user
rescue ActiveRecord::RecordNotFound => e
render json: { errors: e.message }, status: :unauthorized
rescue JWT::DecodeError => e
render json: { errors: e.message }, status: :unauthorized
end
end
end
# How to Use
class PostsController < ApplicationController
before_action :authorize_request
end
class ApplicationController < ActionController::API
def is_owner user_id
unless user_id == @current_user.id
render json: nil, status: :forbidden
return
end
end
def is_owner_object data
if data.nil? or data.user_id.nil?
return render status: :not_found
else
is_owner data.user_id
end
end
end
# How to Use
class PostsController < ApplicationController
before_action only: [:update, :destroy, :destroy_attached] do
is_owner_object @post ##your object
end
end
- Gemfile
gem 'active_model_serializers'
- Generate Serializer
- Generate Serializer to Exist Model user, post
rails g serializer user name:string username:string email:string rails g serializer post body:string user:references published:boolean
- Generate Serializer New Model comment
rails g scaffold comment body:string post:references user:references published:boolean rails g serializer comment body:string user:references published:boolean
- Generate Serializer to Exist Model user, post
- Add Model Attribute
# app/serializers/post_serializer.rb class PostSerializer < ActiveModel::Serializer attributes :id, :body, :user, :comments has_one :user has_many :comments end
# app/serializers/comment_serializer.rb class CommentSerializer < ActiveModel::Serializer attributes :id, :body, :user has_one :user end
# app/serializers/user_serializer.rb class UserSerializer < ActiveModel::Serializer attributes :id, :name, :username, :email end
- For Nested model serializer
# config/initializers/active_model_serializer.rb ActiveModelSerializers.config.default_includes = '**'
- Pagination with serializer
/app/helpers/category_helper.rb
# app/helpers/category_helper.rb
module CategoryHelper
def fetch_categories pagaination_param
page = pagaination_param[:category_page]
per = pagaination_param[:category_per]
key = "categories"+pagaination_param.to_s
categories = $redis.get(key)
if categories.nil?
@categories = Category.published.by_date.page(page).per(per)
categories = Pagination.build_json(@categories, pagaination_param).to_json
$redis.set(key, categories)
$redis.expire(key, 1.hour.to_i)
end
categories
end
def clear_cache_categories
keys = $redis.keys "*categories*"
keys.each {|key| $redis.del key}
end
end
class CategoriesController < ApplicationController
include CategoryHelper
//... your code
# GET /categories
def index
page = params[:page].presence || 1
per = params[:per].presence || Pagination.per
pagaination_param = {
category_page: page,
category_per: per,
post_page: @post_page,
post_per: @post_per
}
@categories = fetch_categories pagaination_param
render json: @categories
end
class Category < ApplicationRecord
include CategoryHelper
belongs_to :user
has_many :posts
scope :published, -> { where(published: true) }
scope :by_date, -> { order('id DESC') }
validates :title, presence: true
validates :body, presence: true
after_save :clear_cache_categories
end
class CategorySerializer < ActiveModel::Serializer
attributes :id, :title, :body, :posts_pagination
has_one :user
has_many :posts
def posts
post_page = (instance_options.dig(:pagaination_param, :post_page).presence || 1).to_i
post_per = (instance_options.dig(:pagaination_param, :post_per).presence || 0).to_i
object.posts.published.by_date.page(post_page).per(post_per)
end
def posts_pagination
post_per = (instance_options.dig(:pagaination_param, :post_per).presence || Pagination.per).to_i
Pagination.build_json(posts)[:posts_pagination] if post_per > 0
end
end
# /lib/pagination.rb
class Pagination
def self.information array
{
current_page: array.current_page,
next_page: array.next_page,
prev_page: array.prev_page,
total_pages: array.total_pages,
total_count: array.total_count
}
end
def self.build_json array, pagaination_param = {}
ob_name = array.name.downcase.pluralize.to_sym
json = Hash.new
json[ob_name] = ActiveModelSerializers::SerializableResource.new(array.to_a, pagaination_param: pagaination_param)
pagination_name = "#{ob_name}_pagination".to_sym
json[pagination_name] = self.information array
json
end
end
- Comment Controller
class CommentsController < ApplicationController before_action :authorize_request before_action :set_comment, only: [:show, :update, :destroy] before_action only: [:edit, :update, :destroy] do is_owner_object @comment ##your object end //...your code # Only allow a trusted parameter "white list" through. def comment_params params.require(:comment).permit(:body, :post_id).merge(user_id: @current_user.id) end end
- Model
# app/models/post.rb class Post < ApplicationRecord belongs_to :user has_many :comments end
# app/models/comment.rb class Comment < ApplicationRecord belongs_to :post belongs_to :user end
- alter column
$ rails generate migration ChangePublishedDefaultToComments published:boolean
class ChangePublishedDefaultToComments < ActiveRecord::Migration[6.0] def change change_column :comments, :published, :boolean, default: true end end
- Add
published = true
condition for has_many In Model Serializerclass PostSerializer < ActiveModel::Serializer attributes :id, :body has_one :user has_many :comments def comments object.comments.where(published: true).order('id DESC') end end
- generate
rails g scaffold category title:string body:string user:references published:boolean
- add referer
rails g migration AddCategoryToPosts category:references
- migration
# db/seed.rb user = User.create!({username: 'hello', email: '[email protected]', password: 'hhhhhhhhh', password_confirmation: 'hhhhhhhhh'}) category = Category.create!({title: 'all', body: 'you can talk everything', user_id: user.id}) posts = Post.where(category_id: nil).or(Post.where(published: nil)) posts.each do |post| post.category_id = category.id post.published = true post.save p post end p category
rake db:seed
- add title column
rails g migration AddTitleToPosts title:string
- remove column
rails g migration RemoveColumnFromTables column:type
- add column
rails g migration AddColumnFromTables column:type
- add unique to name
rails g migration AddUniqueNameToUsers
sample - add unique to user.name in generate file
add_index :table_name, :column_name, unique: true
https://rubyinrails.com/2018/11/10/rails-building-json-api-resopnses-with-jbuilder/
```ruby
gem 'faker', '~> 1.9.1', group: [:development, :test]
```
-
Join User
curl -d '{"user": {"name":"ChangWoo", "username":"CW", "email":"[email protected]", "password":"hello1234", "password_confirmation":"hello1234"}}' -H "Content-Type: application/json" -X POST -i http://localhost:3000/users curl -d '{"user": {"name":"hihi", "username":"helloworld", "email":"[email protected]", "password":"hello1234", "password_confirmation":"hello1234"}}' -H "Content-Type: application/json" -X POST -i http://localhost:3000/users
-
Login
curl -d '{"email":"[email protected]", "password":"hello1234"}' -H "Content-Type: application/json" -X POST http://localhost:3000/auth/login | jq curl -d '{"email":"[email protected]", "password":"hello1234"}' -H "Content-Type: application/json" -X POST http://localhost:3000/auth/login | jq
-
Create Post
curl -X POST -i http://localhost:3000/posts -d '{"post": {"body":"sample body text sample"}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY" curl -X POST -i http://localhost:3000/posts -d '{"post": {"body":"hihihi ahaha"}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY" curl -X POST -i http://localhost:3000/posts -d '{"post": {"body":"Average Speed Time Time Time Current"}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE1Nzc0OTMwMjl9.s9WqkyM84LQGZUtpmfmZzWN8rsVUp4_yfKfxEN_t4AQ"
file upload - create
curl -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODE1MjgwNjd9.YKkk0B-T0_AROBTVaQ7f_OE2hnFGp1HcR2wbEDa9EtA" \ -F "post[body]=string123" \ -F "post[category_id]=1" \ -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \ -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \ -X POST http://localhost:3000/api/v1/posts
file upload - delete
curl -X DELETE "http://localhost:3000/api/v1/posts/731/attached/93" \ -H "accept: application/json" \ -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODE1NDY0Njl9.XjaDElIlvmWDyAWMiGtjZByax-IuG1HBn3i8-Rjl1EU"
file upload - update
curl -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODE1MjgwNjd9.YKkk0B-T0_AROBTVaQ7f_OE2hnFGp1HcR2wbEDa9EtA" \ -F "post[body]=aasadsadasdasstring123" \ -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \ -F "post[files][]=@/Users/rhee/Desktop/item/log/47310817701116.csv" \ -X PUT http://localhost:3000/api/v1/posts/728
-
Index Post
curl -X GET http://localhost:3000/posts -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY" | jq
-
Create Comment
curl -X POST -i http://localhost:3000/comments -d '{"comment": {"body":"sample body for comment", "post_id": 2}}' -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1Nzc0OTAyNjJ9.PCY7kXIlImORySIeDd78gErhqApAyGP6aNFBmK_mdXY"
-
loop curl
for i in {1..100}; do bundle exec rspec; done for i in {1..10000}; do curl -X GET "http://localhost:3000/categories?page=1" -H "accept: application/json" -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODA1NzY5NDZ9.vjoQpeOKdX83JwAwkPBi6p-dWjc1MPGVUQsSG9QSWhg"; done ab -n 10000 -c 100 -H "accept: application/json" -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODA1NzY5NDZ9.vjoQpeOKdX83JwAwkPBi6p-dWjc1MPGVUQsSG9QSWhg" -v 2 http://localhost:3000/categories?page=1
if you want stop for loop
pkill rspec
login sample curl
curl -w "\n" -X POST "http://localhost:3000/auth/login" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"email\": \"[email protected]\", \"password\": \"hello1234\"}" >> curl.log
-
testing
bundle exec rspec spec/requests/users_spec.rb --format documentation
-
Generate documentation
this command
rake rswag
will generate swag documentation. then you can connect to http://localhost:3000/api-docs/index.html
We developed server side code and We shoud need Client code. you can use Swagger-Codegen https://github.com/swagger-api/swagger-codegen#swagger-code-generator
https://github.com/swagger-api/swagger-codegen/wiki/FAQ#how-can-i-generate-an-android-sdk
brew install swagger-codegen
mkdir -p /var/tmp/java/okhttp-gson/
swagger-codegen generate -i http://localhost:3000/api-docs/v1/swagger.yaml \
-l java --library=okhttp-gson \
-D hideGenerationTimestamp=true \
-o /var/tmp/java/okhttp-gson/
Caused by: android.os.NetworkOnMainThreadException https://www.toptal.com/android/android-threading-all-you-need-to-know
sample https://i.stack.imgur.com/ytin1.png https://gist.github.com/just-kip/1376527af60c74b07bef7bd7f136ff56
AsyncTask<Post, Void, Post> asyncTask = new AsyncTask<Post, Void, Post>() {
@Override
protected Post doInBackground(Post... params) {
try {
ApiClient defaultClient = Configuration.getDefaultApiClient();
String authorization = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODIwOTg3NzF9.JGPR2oOOeGcjSocU4Ohvw1bg49ZjTQ9tQ3FtxmqmPDM"; // String | JWT token for Authorization
ApiKeyAuth Bearer = (ApiKeyAuth) defaultClient.getAuthentication("Bearer");
Bearer.setApiKey(authorization);
PostApi apiInstance = new PostApi();
String id = "1"; // String | id
Integer commentPage = 1; // Integer | Page number for Comment
Integer commentPer = 10; // Integer | Per page number For Comment
Post result;
try {
result = apiInstance.apiV1PostsIdGet(id, authorization, commentPage, commentPer);
// System.out.println(result);
} catch (ApiException e) {
System.err.println("Exception when calling PostApi#apiV1PostsIdGet");
e.printStackTrace();
result = new Post();
}
return result;
} catch (Exception e) {
e.printStackTrace();
return new Post();
}
}
@Override
protected void onPostExecute(Post post) {
super.onPostExecute(post);
if (post != null) {
mEmailView.setText(post.getBody());
System.out.print(post);
}
}
};
asyncTask.execute();
https://www.sitepoint.com/consuming-web-apis-in-android-with-okhttp/
No Network Security Config specified, using platform default Use 10.0.2.2 to access your actual machine. https://stackoverflow.com/questions/5528850/how-do-you-connect-localhost-in-the-android-emulator // private String basePath = "https://tutorial-rails-rest-api.herokuapp.com"; // private String basePath = "http://localhost:3000"; private String basePath = "http://10.0.2.2:3000";
swagger-annotations Unable to pre-dex https://stackoverflow.com/questions/43997544/execution-failed-for-task-java-lang-runtimeexceptionunable-to-pre-dex
dexOptions {
javaMaxHeapSize "2g" // set it to 4g will bring unable to start JavaVirtualMachine
preDexLibraries = false
}
enable value is
false
ortrue
exmaple :enable: false
elk.yml
# config/elk.yml
default: &default
enable: false
protocal: udp
host: localhost
port: 5000
development:
<<: *default
test:
<<: *default
production:
<<: *default
https://github.com/roidrage/lograge
https://ericlondon.com/2017/01/26/integrate-rails-logs-with-elasticsearch-logstash-kibana-in-docker-compose.html
lograge.rb
Rails.application.configure do
enable = Rails.configuration.elk['enable']
protocal = Rails.configuration.elk['protocal']
host = Rails.configuration.elk['host']
port = Rails.configuration.elk['port']
if enable
config.autoflush_log = true
config.lograge.base_controller_class = 'ActionController::API'
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Logstash.new
config.lograge.logger = LogStashLogger.new(type: protocal, host: host, port: port, sync: true)
config.lograge.custom_options = lambda do |event|
exceptions = %w(controller action format id)
{
type: :rails,
environment: Rails.env,
remote_ip: event.payload[:ip],
email: event.payload[:email],
user_id: event.payload[:user_id],
request: {
headers: event.payload[:headers],
params: event.payload[:params].except(*exceptions)
}
}
end
end
end
application.rb https://guides.rubyonrails.org/v4.2/configuring.html#custom-configuration
override append_info_to_payload for lograge, append_info_to_payload method put parameter to payload[]
class ApplicationController < ActionController::API
#...leave out the details
def append_info_to_payload(payload)
super
payload[:ip] = remote_ip(request)
if @current_user.present?
begin
user = User.find(@current_user.id)
payload[:email] = user.email
payload[:user_id] = user.id
rescue ActiveRecord::RecordNotFound => e
payload[:email] = ''
payload[:user_id] = ''
end
end
end
def remote_ip(request)
request.headers['HTTP_X_REAL_IP'] || request.remote_ip
end
end
docker run --rm --name my-redis-container -p 7001:6379 -d redis redis-server --appendonly yes
docker run --rm --name my-redis-container -p 7001:6379 -d redis
redis-cli -h localhost -p 7001
gem 'redis'
gem 'redis-namespace'
gem 'redis-rails'
gem 'redis-rack-cache'
# config/initializers/redis.rb
$redis = Redis::Namespace.new("tutorial_post", :redis => Redis.new(:host => '127.0.0.1', :port => 7001))
# GET /categories
def index
page = params[:page].presence || 1
per = params[:per].presence || Pagination.per
pagaination_param = {
category_page: page,
category_per: per,
post_page: @post_page,
post_per: @post_per
}
@categories = fetch_categories pagaination_param
render json: @categories
end
class Category < ApplicationRecord
include CategoryHelper
...your code
after_save :clear_cache_categories
end
# app/helpers/category_helper.rb
module CategoryHelper
def fetch_categories pagaination_param
page = pagaination_param[:category_page]
per = pagaination_param[:category_per]
key = "categories"+pagaination_param.to_s
categories = $redis.get(key)
if categories.nil?
@categories = Category.published.by_date.page(page).per(per)
categories = Pagination.build_json(@categories, pagaination_param).to_json
$redis.set(key, categories)
$redis.expire(key, 1.hour.to_i)
end
categories
end
def clear_cache_categories
keys = $redis.keys "*categories*"
keys.each {|key| $redis.del key}
end
end
https://cameronbothner.com/activestorage-beyond-rails-views/ https://edgeguides.rubyonrails.org/active_storage_overview.html
https://edgeguides.rubyonrails.org/active_storage_overview.html#has-many-attached
rails active_storage:install
rake db:migrate
storage.yml you wiil make dir /storage with
mkdir /storage
test:
service: Disk
root: /storage/test
local:
service: Disk
root: /storage
routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# your code will be here ...
# DELETE /posts/attached/:attached_id
delete '/posts/:id/attached/:attached_id', to: 'posts#destroy_attached'
end
end
end
posts_controller.rb
@post.files.attach(params[:post][:files]) if params.dig(:post, :files).present?
is null check and add files
# posts_controller
# POST /posts
def create
@post = Post.new(post_params)
@post.files.attach(params[:post][:files]) if params.dig(:post, :files).present?
set_category @post.category_id
if @post.save
render json: @post, status: :created, location: api_v1_post_url(@post)
else
render json: @post.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /posts/1
def update
@post.files.attach(params[:post][:files]) if params.dig(:post, :files).present?
if @post.update(post_params)
render json: @post
else
render json: @post.errors, status: :unprocessable_entity
end
end
# DELETE /posts/:id/attached/:id
def destroy_attached
attachment = ActiveStorage::Attachment.find(params[:attached_id])
attachment.purge # or use purge_later
end
post.rb
class Post < ApplicationRecord
# your code will be here ...
has_many_attached :files
end
class PostSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :body, :files, :comments_pagination
def files
return unless object.files.attachments
file_urls = object.files.map do |file|
{
id: file.id,
url: rails_blob_url(file)
}
end
file_urls
end
# your code will be here ...
end
https://gist.github.com/kelvinn/6a1c51b8976acf25bd78
bash ab -c 10 -n 10000 \ -T application/json \ -H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1OTA0MzExOTN9.GtlH3xbINMNSKAU00np5njGtDWEcXXOHZ2zbjKsgr24" \ http://localhost:3000/api/v1/posts?category_id=1&page=1