Peeps-UUIDs is a very basic contact management system implemented as an API that follows the JSON API spec. Peeps-UUIDs is based on https://github.com/cerebris/peeps.
Other apps will soon be written to demonstrate writing a consumer for this API.
This app requires that postgresql be installed locally. General instructions are available here for many operating systems.
After cloning this repo, run the following:
bundleEnsure that your config/database.yml is configured properly, and then run:
rake db:create db:migrateStart your server:
rails serverActually, by using docker-compose, no dependencies (including Postgres) need to be installed on your machine.
After cloning the repo, uncomment these three lines in config/database.yml:
host: <%= ENV['POSTGRES_PORT_5432_TCP_ADDR'] %>
port: <%= ENV['POSTGRES_PORT_5432_TCP_PORT'] %>
username: postgres
Next, cd to the root directory and run the following:
docker-compose up --build
This will build and start the two Docker containers: one for Postgres and one for the Rails app. They are set to link ports.
In a new Terminal window, run docker ps to determine which container is running the Rails app. Then run the following:
docker exec -it <CONTAINER ID> /bin/bash
This will open a bash session in that container where you will run this line:
rake db:create db:migrate
You are now all set to go, with the Rails app responding to requests at localhost:3000.
The instructions below were followed to create this app from scratch.
rails new peeps-uuids -d postgresql --skip-javascriptThe default database.yml may not work for your configuration, so you will need to set this up based on your installation.
rake db:createAdd the gem to your Gemfile
gem 'jsonapi-resources'Then bundle
bundleMake the following changes to application_controller.rb
class ApplicationController < ActionController::Base
include JSONAPI::ActsAsResourceController
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session
endOR
class ApplicationController < JSONAPI::ResourceController
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session
endYou can also do this on a per controller basis in your app, if only some controllers will serve the API.
Edit config/environments/development.rb
Eager loading of classes is recommended. The code will work without it, but I think it's the right way to go. See http://blog.plataformatec.com.br/2012/08/eager-loading-for-greater-good/
# Eager load code on boot so JSONAPI-Resources resources are loaded and processed globally
config.eager_load = trueconfig.consider_all_requests_local = falseThis will prevent the server from returning the HTML formatted error messages when an exception happens. Not strictly necessary, but it makes for nicer output when debugging using curl or a client library.
Create a migration to enable UUID support.
rails g migration EnableUuidsEdit the migration
class EnableUuids < ActiveRecord::Migration
def change
enable_extension 'uuid-ossp'
end
endrake db:migrateCreate an initializer, such as config/initializers/jsonapi.rb, that contains the following:
JSONAPI.configure do |config|
# Allowed values are :integer(default), :uuid, :string, or a proc
config.resource_key_type = :uuid
endThis setting could alternatively be made on a per-resource basis.
Use the standard rails generator to create a model for Contacts and one for related PhoneNumbers
rails g model Contact first_name:string last_name:string email:string twitter:stringEdit the migration to set the id to use uuids
class CreateContacts < ActiveRecord::Migration
def change
create_table :contacts, id: :uuid do |t|
t.string :first_name
t.string :last_name
t.string :email
t.string :twitter
t.timestamps
end
end
endEdit the model
class Contact < ApplicationRecord
has_many :phone_numbers
### Validations
validates :first_name, presence: true
validates :last_name, presence: true
def self.creatable_fields(context)
super + [:id]
end
endCreate the PhoneNumber model
rails g model PhoneNumber contact_id:integer name:string phone_number:stringEdit the migration for uuid
class CreatePhoneNumbers < ActiveRecord::Migration
def change
create_table :phone_numbers, id: :uuid do |t|
t.uuid :contact_id
t.string :name
t.string :phone_number
t.timestamps
end
end
endEdit the model
class PhoneNumber < ApplicationRecord
belongs_to :contact
def self.creatable_fields(context)
super + [:id]
end
endrake db:migrateUse the rails generator to create empty controllers. These will be inherit methods from the ResourceController so they will know how to respond to the standard REST methods.
rails g controller Contacts --skip-assets
rails g controller PhoneNumbers --skip-assetsWe need a directory to hold our resources. Let's put in under our app directory
mkdir app/resourcesCreate a new file for each resource. This must be named in a standard way so it can be found. This should be the single underscored name of the model with _resource.rb appended. For Contacts this will be contact_resource.rb.
Make the two resource files
contact_resource.rb
class ContactResource < JSONAPI::Resource
attributes :first_name, :last_name, :email, :twitter
has_many :phone_numbers
endand phone_number_resource.rb
class PhoneNumberResource < JSONAPI::Resource
attributes :name, :phone_number
has_one :contact
filter :contact
endRequire jsonapi/routing_ext
require 'jsonapi/routing_ext'Add the routes for the new resources
UUID_regex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(,[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})*/
jsonapi_resources :contacts, constraints: {:id => UUID_regex}
jsonapi_resources :phone_numbers, constraints: {:id => UUID_regex}Launch the app
rails serverCreate a new contact
curl -i -H "Accept: application/vnd.api+json" -H 'Content-Type:application/vnd.api+json' -X POST -d '{"data": {"type":"contacts", "attributes":{"first-name":"John", "last-name":"Doe", "email":"[email protected]"}}}' http://localhost:3000/contactsYou should get something like this back
HTTP/1.1 201 Created
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
Location: http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2
ETag: W/"1b5c63402a02363d3985132d8298bcee"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 34785427-6427-43ec-8fc7-34dc64a17e5b
X-Runtime: 0.023691
Transfer-Encoding: chunked
{"data":{"id":"77eec4e9-4244-492d-8340-18892e2c54b2","type":"contacts","links":{"self":"http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2"},"attributes":{"first-name":"John","last-name":"Doe","email":"[email protected]","twitter":null},"relationships":{"phone-numbers":{"links":{"self":"http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2/relationships/phone-numbers","related":"http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2/phone-numbers"}}}}}
You can now create a phone number for this contact
curl -i -H "Accept: application/vnd.api+json" -H 'Content-Type:application/vnd.api+json' -X POST -d '{ "data": { "type": "phone-numbers", "relationships": { "contact": { "data": { "type": "contacts", "id": "77eec4e9-4244-492d-8340-18892e2c54b2" } } }, "attributes": { "name": "home", "phone-number": "(603) 555-1212" } } }' http://localhost:3000/phone-numbers
And you should get back something like this:
HTTP/1.1 201 Created
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
Location: http://localhost:3000/phone-numbers/13a8befe-8958-49ed-9b94-a71768986465
ETag: W/"878009bdc1ac40d69706504a48ed49eb"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 3e345e2b-3ecf-44a4-92e6-c943e26413da
X-Runtime: 0.024379
Transfer-Encoding: chunked
{"data":{"id":"13a8befe-8958-49ed-9b94-a71768986465","type":"phone-numbers","links":{"self":"http://localhost:3000/phone-numbers/13a8befe-8958-49ed-9b94-a71768986465"},"attributes":{"name":"home","phone-number":"(603) 555-1212"},"relationships":{"contact":{"links":{"self":"http://localhost:3000/phone-numbers/13a8befe-8958-49ed-9b94-a71768986465/relationships/contact","related":"http://localhost:3000/phone-numbers/13a8befe-8958-49ed-9b94-a71768986465/contact"}}}}}
You can now query all one of your contacts
curl -i -H "Accept: application/vnd.api+json" "http://localhost:3000/contacts"And you get this back:
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"aec78dabf2895bf6c4a9a7c4374e881e"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0541fb25-6493-4d6a-a78a-2d3bf4b78330
X-Runtime: 0.005499
Transfer-Encoding: chunked
{"data":[{"id":"77eec4e9-4244-492d-8340-18892e2c54b2","type":"contacts","links":{"self":"http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2"},"attributes":{"first-name":"John","last-name":"Doe","email":"[email protected]","twitter":null},"relationships":{"phone-numbers":{"links":{"self":"http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2/relationships/phone-numbers","related":"http://localhost:3000/contacts/77eec4e9-4244-492d-8340-18892e2c54b2/phone-numbers"}}}}]}
Note that the phone_number id is included in the links, but not the details of the phone number. You can get these by setting an include:
curl -i -H "Accept: application/vnd.api+json" "http://localhost:3000/contacts?include=phone-numbers"and some fields:
curl -i -H "Accept: application/vnd.api+json" "http://localhost:3000/contacts?include=phone-numbers&fields%5Bcontacts%5D=fist-name,last-name&fields%5Bphone-numbers%5D=name"Test a validation Error
curl -i -H "Accept: application/vnd.api+json" -H 'Content-Type:application/vnd.api+json' -X POST -d '{ "data": { "type": "contacts", "attributes": { "first-name": "John Doe", "email": "[email protected]" } } }' http://localhost:3000/contacts