https://qiita.com/YusukeIwaki/items/2936b2b79a113c9b495d のAPI版。
完全に自分用メモ。
リポジトリを作る
rails new の前に
既存のワークスペースは消す
yusukeiwaki:~/workspace $ cd ..
yusukeiwaki:~ $ rm -rf workspace/
yusukeiwaki:~ $ mkdir workspace
yusukeiwaki:~ $ cd workspace/
最新のrailsをインストール
yusukeiwaki:~/workspace $ bundle init
Writing new Gemfile to /home/ubuntu/workspace/Gemfile
Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "rails"
のように編集(gem rails のコメントアウトを外す)して、
yusukeiwaki:~/workspace $ bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies...
Fetching rake 12.3.1
Installing rake 12.3.1
Using concurrent-ruby 1.0.5
Fetching minitest 5.11.3
Installing minitest 5.11.3
Using thread_safe 0.3.6
Using builder 3.2.3
Fetching erubi 1.7.1
Installing erubi 1.7.1
Fetching mini_portile2 2.3.0
Installing mini_portile2 2.3.0
(省略)
rails new
今回はAPIなので、Rails 5から導入されたAPIモードを使う。
yusukeiwaki:~/workspace $ bundle exec rails new --api --database mysql --skip-test --skip-action-cable --skip-keeps --skip-yarn .
exist
create README.md
create Rakefile
create .ruby-version
create config.ru
create .gitignore
conflict Gemfile
Overwrite /home/ubuntu/workspace/Gemfile? (enter "h" for help) [Ynaqdh] y
force Gemfile
run git init from "."
Initialized empty Git repository in /home/ubuntu/workspace/.git/
create app
create app/assets/config/manifest.js
create app/assets/javascripts/application.js
create app/assets/javascripts/cable.js
create app/assets/stylesheets/application.css
create app/channels/application_cable/channel.rb
create app/channels/application_cable/connection.rb
create app/controllers/application_controller.rb
create app/helpers/application_helper.rb
create app/jobs/application_job.rb
create app/mailers/application_mailer.rb
create app/models/application_record.rb
create app/views/layouts/application.html.erb
create app/views/layouts/mailer.html.erb
create app/views/layouts/mailer.text.erb
create bin
create bin/bundle
create bin/rails
create bin/rake
create bin/setup
create bin/update
create bin/yarn
create config
create config/routes.rb
create config/application.rb
create config/environment.rb
create config/puma.rb
create config/spring.rb
create config/storage.yml
create config/environments
create config/environments/development.rb
create config/environments/production.rb
create config/environments/test.rb
create config/initializers
create config/initializers/application_controller_renderer.rb
create config/initializers/assets.rb
create config/initializers/backtrace_silencers.rb
create config/initializers/content_security_policy.rb
create config/initializers/cookies_serializer.rb
create config/initializers/cors.rb
create config/initializers/filter_parameter_logging.rb
create config/initializers/inflections.rb
create config/initializers/mime_types.rb
create config/initializers/new_framework_defaults_5_2.rb
create config/initializers/wrap_parameters.rb
create config/locales
create config/locales/en.yml
create config/master.key
append .gitignore
create config/boot.rb
create config/database.yml
create db
create db/seeds.rb
create lib
create lib/tasks
create lib/assets
create log
create public
create public/404.html
create public/422.html
create public/500.html
create public/apple-touch-icon-precomposed.png
create public/apple-touch-icon.png
create public/favicon.ico
create public/robots.txt
create tmp
create tmp/cache
create tmp/cache/assets
create vendor
create storage
create tmp/storage
remove app/assets
remove lib/assets
remove tmp/cache/assets
remove app/helpers
remove test/helpers
remove app/views/layouts/application.html.erb
remove public/404.html
remove public/422.html
remove public/500.html
remove public/apple-touch-icon-precomposed.png
remove public/apple-touch-icon.png
remove public/favicon.ico
remove app/assets/javascripts
remove config/initializers/assets.rb
remove app/assets/javascripts/cable.js
remove app/channels
remove config/initializers/cookies_serializer.rb
remove config/initializers/content_security_policy.rb
remove config/initializers/new_framework_defaults_5_2.rb
remove bin/yarn
run bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/.........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Using rake 12.3.1
Using concurrent-ruby 1.0.5
Using i18n 1.0.1
Using minitest 5.11.3
Using thread_safe 0.3.6
Using tzinfo 1.2.5
Using activesupport 5.2.0
Using builder 3.2.3
Using erubi 1.7.1
Using mini_portile2 2.3.0
Using nokogiri 1.8.2
Using rails-dom-testing 2.0.3
Using crass 1.0.4
Using loofah 2.2.2
Using rails-html-sanitizer 1.0.4
Using actionview 5.2.0
Using rack 2.0.5
Using rack-test 1.0.0
Using actionpack 5.2.0
Using nio4r 2.3.1
Using websocket-extensions 0.1.3
Using websocket-driver 0.7.0
Using actioncable 5.2.0
Using globalid 0.4.1
Using activejob 5.2.0
Using mini_mime 1.0.0
Using mail 2.7.0
Using actionmailer 5.2.0
Using activemodel 5.2.0
Using arel 9.0.0
Using activerecord 5.2.0
Using mimemagic 0.3.2
Using marcel 0.3.2
Using activestorage 5.2.0
Using msgpack 1.2.4
Using bootsnap 1.3.0
Using bundler 1.16.1
Using byebug 10.0.2
Using ffi 1.9.23
Using rb-fsevent 0.10.3
Using rb-inotify 0.9.10
Using ruby_dep 1.5.0
Using listen 3.1.5
Using method_source 0.9.0
Using mysql2 0.5.1
Using puma 3.11.4
Using thor 0.20.0
Using railties 5.2.0
Using sprockets 3.7.1
Using sprockets-rails 3.2.1
Using rails 5.2.0
Using spring 2.0.2
Using spring-watcher-listen 2.0.1
Bundle complete! 9 Gemfile dependencies, 53 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
run bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted
めも:
-
--skip-javascript
は指定しても多分きかない。 -
--skip-action-cable
はAPIモードだと効かない? →issue
なんとなくバージョン管理
yusukeiwaki:~/workspace (master) $ git add -A
yusukeiwaki:~/workspace (master) $ git commit -a -m "initial commit"
config/database.ymlの編集
Cloud9環境にあわせて、以下のようにする
config/database.yml
default: &default
adapter: mysql2
charset: utf8mb4
encoding: utf8mb4
collation: utf8mb4_unicode_ci
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
socket: /var/run/mysqld/mysqld.sock
development:
<<: *default
database: c9
username: <%=ENV['C9_USER']%>
host: <%=ENV['IP']%>
test:
<<: *default
database: c9_test
username: <%=ENV['C9_USER']%>
host: <%=ENV['IP']%>
ちなみに、Cloud9環境かどうかに関係なく、 charset: utf8
のデフォルト設定は良くないので、特別な理由がない限り
charset: utf8mb4
encoding: utf8mb4
collation: utf8mb4_unicode_ci
に変更する。
ためしに起動する
MySQLサーバの起動
Cloud9特有。
yusukeiwaki:~/workspace (master) $ mysql-ctl start
Installing MySQL
* Stopping MySQL database server mysqld
...done.
* Starting MySQL database server mysqld
...done.
* Checking for tables which need an upgrade, are corrupt or were
not closed cleanly.
MySQL 5.5 database added. Please make note of these credentials:
Root User: yusukeiwaki
Database Name: c9
* Starting MySQL database server mysqld
...done.
Railsサーバの起動
うん、まったくAPIっぽくないw
てきとうなAPIをつくる
Memoモデル(title/body)の追加、一覧、削除APIをつくる。
モデルをつくる
yusukeiwaki:~/workspace (master) $ bundle exec rails g model Memo title:string body:text
Running via Spring preloader in process 19767
invoke active_record
create db/migrate/20180512131906_create_memos.rb
create app/models/memo.rb
yusukeiwaki:~/workspace (master) $ bundle exec rake db:migrate
== 20180512131906 CreateMemos: migrating ======================================
-- create_table(:memos)
-> 0.0291s
== 20180512131906 CreateMemos: migrated (0.0292s) =============================
ルートを書く
config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
namespace :api, defaults: { format: :json } do
resources :memos, only: [:index, :create, :destroy]
end
end
yusukeiwaki:~/workspace (master) $ bundle exec rake routes
Prefix Verb URI Pattern Controller#Action
api_memos GET /api/memos(.:format) api/memos#index {:format=>:json}
POST /api/memos(.:format) api/memos#create {:format=>:json}
api_memo DELETE /api/memos/:id(.:format) api/memos#destroy {:format=>:json}
コントローラを書く
api/application_controller.rb
class Api::ApplicationController < ::ApplicationController
rescue_from ActiveRecord::RecordInvalid, with: :handle_validation_error
rescue_from ActionController::ParameterMissing, with: :handle_bad_request
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
private
def handle_validation_error(err)
render_error(400, err.record.errors.full_messages.join("\n"))
end
def handle_bad_request(err)
render_error(400, err.message)
end
def handle_not_found(err)
render_error(404, "Not Found")
end
def render_error(status, message)
render json: { error: message }, status: status
end
end
api/memos_controller.rb
class Api::MemosController < Api::ApplicationController
def index
render json: Memo.all.map{ |memo| memo.attributes.slice("id", "title", "body") }
end
def create
memo = Memo.create!(params.require(:memo).permit(:title, :body))
render json: memo.attributes.slice("id", "title", "body")
end
def destroy
memo = Memo.destroy(params[:id])
render json: memo.attributes.slice("id", "title", "body")
end
end
app/views にjbuilderで書くのはあとでやる。
yusukeiwaki:~/workspace (master) $ jo memo=$(jo title=title1 body=bodybodybody) | http POST http://localhost:8080/api/memos
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"8d623642bb179d19ecda3be45134d104"
Transfer-Encoding: chunked
X-Request-Id: 41ac3276-b89b-400c-a107-ee134d9c3c21
X-Runtime: 0.050641
{
"body": "bodybodybody",
"id": 1,
"title": "title1"
}
yusukeiwaki:~/workspace (master) $ jo memo=$(jo title=title2 body=bodybody222222) | http POST http://localhost:8080/api/memos
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"7a9ae0e7984992ce75b982487740e4c7"
Transfer-Encoding: chunked
X-Request-Id: 1444f298-0c29-4d33-854a-5185590b2246
X-Runtime: 0.019321
{
"body": "bodybody222222",
"id": 2,
"title": "title2"
}
yusukeiwaki:~/workspace (master) $ http http://localhost:8080/api/memos
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"f43cf704215a7e9ef462e1a5eb5c39ad"
Transfer-Encoding: chunked
X-Request-Id: c43f496e-d342-4d69-8741-6245a52f41cf
X-Runtime: 0.030159
[
{
"body": "bodybodybody",
"id": 1,
"title": "title1"
},
{
"body": "bodybody222222",
"id": 2,
"title": "title2"
}
]
yusukeiwaki:~/workspace (master) $ http DELETE http://localhost:8080/api/memos/1
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"8d623642bb179d19ecda3be45134d104"
Transfer-Encoding: chunked
X-Request-Id: d87cc67b-5c4c-433d-bbb6-51fd84c356f7
X-Runtime: 0.021260
{
"body": "bodybodybody",
"id": 1,
"title": "title1"
}
yusukeiwaki:~/workspace (master) $ http http://localhost:8080/api/memos
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"511bd49242014136687a70c928d57a57"
Transfer-Encoding: chunked
X-Request-Id: d668aeb1-f45a-4f63-8268-e0d6f2a0ad1e
X-Runtime: 0.007397
[
{
"body": "bodybody222222",
"id": 2,
"title": "title2"
}
]
もうすこしいろいろまともにする
モデルのバリデーション
app/models/memo.rb
class Memo < ApplicationRecord
validates :title,
presence: true
validates :body,
presence: true
end
yusukeiwaki:~/workspace (master) $ jo memo=$(jo body=タイトルがないの) | http POST http://localhost:8080/api/memos
HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
X-Request-Id: 8a1694d4-457c-4886-b796-2b90e1945c68
X-Runtime: 0.134392
{
"error": "Title can't be blank"
}
jbuilder
コントローラにJSONレンダリングロジックを書くのはあまりにひどいので、app/viewsにjbuilderで書こう。
Gemfile
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
yusukeiwaki:~/workspace (master) $ bundle install
app/controllers/api/memos_controller.rb
class Api::MemosController < Api::ApplicationController
def index
@memos = Memo.all
end
def create
@memo = Memo.create!(params.require(:memo).permit(:title, :body))
render action: :show
end
def destroy
@memo = Memo.destroy(params[:id])
render action: :show
end
end
app/views/api/memos/index.json.jbuilder
json.memos @memos do |memo|
json.partial! 'memo', memo: memo
end
app/views/api/memos/show.json.jbuilder
json.memo do
json.partial! 'memo', memo: @memo
end
app/views/api/memos/_memo.json.jbuilder
json.extract! memo, :id, :title, :body
yusukeiwaki:~/workspace (master) $ jo memo=$(jo title=hoge body=bodybody) | http POST http://localhost:8080/api/memos
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"fc357235fdc57ac2da064861d82be141"
Transfer-Encoding: chunked
X-Request-Id: 024d313c-6dbf-492a-b96b-d425042eded9
X-Runtime: 0.037340
{
"memo": {
"body": "bodybody",
"id": 3,
"title": "hoge"
}
}
yusukeiwaki:~/workspace (master) $ http DELETE http://localhost:8080/api/memos/2
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"7d5d68be0f2e4981e1dcd5a51cdde183"
Transfer-Encoding: chunked
X-Request-Id: 6f7b6d66-47db-42dc-9011-44db9cccde2f
X-Runtime: 0.041892
{
"memo": {
"body": "bodybody222222",
"id": 2,
"title": "title2"
}
}
yusukeiwaki:~/workspace (master) $ http http://localhost:8080/api/memos
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/json; charset=utf-8
ETag: W/"97eb4b999de3668ab7e126ae2604ae5e"
Transfer-Encoding: chunked
X-Request-Id: d21c707d-c53c-4157-9e3e-25c8a75a649c
X-Runtime: 0.008422
{
"memos": [
{
"body": "bodybody",
"id": 3,
"title": "hoge"
}
]
}
RSpec
specはてきとうでもいいから書いておこう
インストール
group :development, :test do
gem 'rspec-rails', '~> 3.7'
gem 'json_spec', '~> 1.1', '>= 1.1.4'
end
yusukeiwaki:~/workspace (master) $ bundle install
yusukeiwaki:~/workspace (master) $ bundle exec rails g rspec:install
Running via Spring preloader in process 27995
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
json_specの設定
spec/rails_helper.rb の
spec/rails_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
をコメントアウトを外して、以下のファイルを作成。
spec/support/json_spec.rb
RSpec.configure do |config|
config.include JsonSpec::Helpers
end
RAILS_ENV=test でdb:create, db:migrate
yusukeiwaki:~/workspace (master) $ bundle exec rake db:create RAILS_ENV=test
Created database 'c9_test'
yusukeiwaki:~/workspace (master) $ bundle exec rake db:migrate RAILS_ENV=test
== 20180512131906 CreateMemos: migrating ======================================
-- create_table(:memos)
-> 0.0193s
== 20180512131906 CreateMemos: migrated (0.0215s) =============================
request specを実装
spec/requests/api/memos_spec.rb
require 'rails_helper'
describe '/api/memos', type: :request do
describe 'GET /api/memos' do
subject { get api_memos_path }
before {
2.times { |i| Memo.create!(title: "memo#{i}", body: "body#{i}") }
}
it {
subject
expect(response).to have_http_status(200)
expect(response.body).to have_json_size(2).at_path("memos")
}
end
describe 'POST /api/memos' do
subject { post api_memos_path, params: { memo: memo_params } }
context 'titleが空のとき' do
let(:memo_params) { { body: "titleがないよ"} }
it {
subject
expect(response).to have_http_status(400)
}
end
context 'bodyが空のとき' do
let(:memo_params) { { title: "bodyがないよー!"} }
it {
subject
expect(response).to have_http_status(400)
}
end
context 'title, bodyが指定されているとき' do
let(:memo_params) { { title: "titletitletitle", body: "bodybodybody" } }
it {
expect{ subject }.to change{ Memo.count }.by(1)
expect(response).to have_http_status(200)
memo = Memo.last
expect(response.body).to be_json_eql(memo.id).at_path("memo/id")
expect(response.body).to be_json_eql(memo.title.to_json).at_path("memo/title")
expect(response.body).to be_json_eql(memo.body.to_json).at_path("memo/body")
}
end
end
describe 'DELETE /api/memos' do
let(:memo) { Memo.last }
subject { delete api_memo_path(memo) }
before {
2.times { |i| Memo.create!(title: "memo#{i}", body: "body#{i}") }
}
it {
expect{ subject }.to change{ Memo.count }.by(-1)
expect(response).to have_http_status(200)
expect(response.body).to be_json_eql(memo.id).at_path("memo/id")
expect{ Memo.find(memo.id) }.to raise_error(ActiveRecord::RecordNotFound)
}
end
end
factory_girlは面倒なので使っていない。
RSpecを実行
yusukeiwaki:~/workspace (master) $ bundle exec rspec spec/requests/api/memos_spec.rb
.....
Finished in 0.32657 seconds (files took 1.51 seconds to load)
5 examples, 0 failures