Rails Engine Requirements
1. Set Up
-
Create a Rails API project called
rails-engine
(make sure you do not set up a “traditional” Rails project with a frontend, this is an API-only project) -
Set Up SimpleCov to track test coverage in your rails-engine API project.
- Set up your
db/seeds.rb
file with the following content:# before running "rake db:seed", do the following: # - put the "rails-engine-development.pgdump" file in db/data/ cmd = "pg_restore --verbose --clean --no-acl --no-owner -h localhost -U $(whoami) -d rails-engine_development db/data/rails-engine-development.pgdump" puts "Loading PostgreSQL Data dump into local database with command:" puts cmd system(cmd)
- Download rails-engine-development.pgdump and move it into the
/db/
folder in another folder called/data/
, so your project files look like this:/app /bin /config /db /data <-- create this folder rails-engine-development.pgdump <-- put the file in the data folder seeds.rb /lib /log etc
- this file is in a binary format and your browser may try to automatically download the file instead of viewing it
-
Run
rake db:{drop,create,migrate,seed}
and you may see lots of output including some warnings/errors frompg_restore
that you can ignore. If you’re unsure about the errors you’re seeing, ask an instructor. - Use a tool like Postico to examine the 6 tables that were created, and build migration files for those tables. Pay careful attention to the data types of each field:
- items
- merchants
- orders
- order_items
- invoices
- transactions
NOTE We updated this process to avoid confusion and taking a significant amount of time; the main learning goals of the project are the Rails API endpoints and business intelligence endpoints in ActiveRecord, not the process of importing CSV data. Avoid starting out with a Rake task to do the import and follow these instructions instead. If in doubt, ask your instructors first.
NOTE If your rails new ...
project name from above is NOT exactly called “rails-engine” you will need to modify the cmd
variable below to change the -d
parameter from rails-engine_development
to <YOUR PROJECT NAME>_development
instead. If you have questions, ask your instructors.
3. API Endpoints
You will need to expose the data through a multitude of API endpoints. All of your endpoints should follow these technical expectations:
- All endpoints should be fully tested for happy path AND sad path. The Rails Driver Spec Harness is not a substitute for writing your own tests.
- All endpoints will expect to return JSON data only
- All endpoints should be exposed under an
api
and version (v1
) namespace (e.g./api/v1/items
) - API will be compliant to the JSON API spec and match our requirements below precisely
- if your tests pass but the Rails Driver spec harness does not, you have done something wrong.
- Controller actions should be limited to only the standard Rails actions and follow good RESTful convention.
- Endpoints such as
GET /api/v1/merchants/find?parameters
will NOT follow RESTful convention, and that’s okay:
module Api
module V1
class MerchantsController
# code omitted
def find
# code omitted
end
end
end
end
This approach can lead to large controllers. For more info on the reasons why, check out this blog post.
Instead try something like this which adheres to the above approach of only using RESTful actions:
module Api
module V1
module Merchants
class SearchController
def show
# code omitted
end
end
end
end
end
Error Responses
If the user causes an error for which you are sending a 400-series error code, the JSON body of the response should follow a similar JSON Spec of a data
element, with a nil
ID, and empty attributes.
As an EXTENSION, customize the error message to use this format instead:
{
"message": "your query could not be completed",
"errors": [
"string of error message one",
"string of error message two",
"etc"
]
}
You can customize the value of the message
element, but the message
element must be present.
The errors
element will always be an array and contain one or more strings of why the user’s request was unsuccessful. Examples will include a “ID was invalid” in the case of a 404, or “the ‘description’ parameter was missing”
RESTful Endpoints, Minimum Requirements:
You will need to expose the following RESTful API endpoints for the following:
- Merchants:
- get all merchants, a maximum of 20 at a time
- get one merchant
- get all items for a given merchant ID
- Items:
- get all items, a maximum of 20 at a time
- get one item
- create an item
- edit an item
- delete an item
- get the merchant data for a given item ID
Non-RESTful Endpoints, Minimum Requirements:
You will get to choose from the following list:
- ONE of following endpoint pairs:
- find one MERCHANT based on search criteria AND find all ITEMS based on search criteria
- OR:
- find one ITEM based on search criteria AND find all MERCHANTS based on search criteria
- FOUR of the following endpoints:
- find a quantity of merchants sorted by descending revenue
- find a quantity of merchants sorted by descending item quantity sold
- total revenue generated in the whole system over a start/end date range
- total revenue for a given merchant
- find a quantity of items sorted by descending revenue
- total revenue of successful invoices which have not yet been shipped
- revenue report, broken down by month in ascending date order
In total, the MINIMUM requirement will be 15 endpoints.
HINT: Invoices must have a successful transaction and shipped to the customer to be considered as revenue.
RESTful: Fetch all Items/Merchants
These “index” endpoints for items and merchants should:
- render a JSON representation of all records of the requested resource, one “page” of data at a time
- always return an array of data, even if one or zero resources are found
- NOT include dependent data of the resource (eg, if you’re fetching merchants, do not send any data about merchant’s items or invoices)
- follow this pattern:
GET /api/v1/<resource>
- allow for the following OPTIONAL query parameters to be sent by the user:
per_page
, an integer value of how many resources should be in the output; defaults to 20 if not specified by the userpage
, an integer value of a “page” of resources to skip before returning data; defaults to 1 if not specified by the user- do not use any third-party gems for pagination
Example use of query parameters:
GET /api/v1/items?per_page=50&page=2
GET /api/v1/merchants?per_page=50&page=2
This should fetch items 51 through 100, since we’re returning 50
per “page”, and we want “page 2
” of data.
Example JSON response for the Merchant resource:
{
"data": [
{
"id": "1",
"type": "merchant",
"attributes": {
"name": "Mike's Awesome Store",
}
},
{
"id": "2",
"type": "merchant",
"attributes": {
"name": "Store of Fate",
}
},
{
"id": "3",
"type": "merchant",
"attributes": {
"name": "This is the limit of my creativity",
}
}
]
}
If a user tries to fetch a page for which there is no data, then data
should report an empty array.
RESTful: Fetch a single record
This endpoint for Items and Merchants should:
- render a JSON representation of the corresponding record, if found
- follow this pattern:
GET /api/v1/<resource>/:id
Example JSON response for the Item resource:
{
"data": {
"id": "1",
"type": "item",
"attributes": {
"name": "Super Widget",
"description": "A most excellent widget of the finest crafting",
"unit_price": 109.99
}
}
}
Note that the unit_price
is sent as numeric data, and not string data.
RESTful: Create an Item
This endpoint should:
- create a record and render a JSON representation of the new Item record.
- follow this pattern:
POST /api/v1/items
- accept the following JSON body with only the following fields:
{
"name": "value1",
"description": "value2",
"unit_price": 100.99,
"merchant_id": 14
}
(Note that the unit price is to be sent as a numeric value, not a string.)
- return an error if any attribute is missing
- should ignore any attributes sent by the user which are not allowed
Example JSON response for the Item resource:
{
"data": {
"id": "16",
"type": "item",
"attributes": {
"name": "Widget",
"description": "High quality widget",
"unit_price": 100.99,
"merchant_id": 14
}
}
}
RESTful: Update an Item
This endpoint should:
- update the corresponding Item (if found) with whichever details are provided by the user
- render a JSON representation of the updated record.
- follow this pattern:
PATCH /api/v1/items/:id
- accept the following JSON body with one or more of the following fields: The body should follow this pattern:
{
"name": "value1",
"description": "value2",
"unit_price": 100.99,
"merchant_id": 7
}
(Note that the unit price is to be sent as a numeric value, not a string.)
Example JSON response for the Item resource:
{
"data": {
"id": "1",
"type": "item",
"attributes": {
"name": "New Widget Name",
"description": "High quality widget, now with more widgety-ness",
"unit_price": 299.99,
"merchant_id": 7
}
}
}
RESTful: Destroy an Item
This endpoint should:
- destroy the corresponding record (if found) and any associated data
- destroy any invoice if this was the only item on an invoice
- NOT return any JSON body at all, and should return a 204 HTTP status code
- NOT utilize a Serializer (Rails will handle sending a 204 on its own if you just
.destroy
the object)
RESTful: Relationship Endpoints
These endpoints should show related records for a given resource. The relationship endpoints you should expose are:
GET /api/v1/merchants/:id/items
- return all items associated with a merchant.- return a 404 if merchant is not found
GET /api/v1/items/:id/merchant
- return the merchant associated with an item- return a 404 if the item is not found
Non-RESTful Search Endpoints
In addition to the standard RESTful endpoints described above, you will build the following endpoints which will NOT follow RESTful convention:
GET /api/vi/items/find_one
, find a single item which matches a search termGET /api/vi/items/find_all
, find all items which match a search termGET /api/vi/merchants/find_one
, find a single merchant which matches a search termGET /api/vi/merchants/find_all
, find all merchants which match a search term
These endpoints will make use of query parameters as described below:
“Find One” endpoints
These endpoints should:
- return a single object, if found
- return the first object in the database in case-sensitive alphabetical order if multiple matches are found
- eg, if “Ring World” and “Turing” exist as merchant names, “Ring World” would be returned, even if “Turing” was created first
- allow the user to specify a ‘name’ query parameter:
- for merchants, the user can send
?name=ring
and it will search thename
field in the database table - for items, the user can send
?name=ring
and it will search thename
field in the database table- this should find a name of ‘Titanium Ring’ and anything with a description like ‘This silver chime will bring you cheer!’
- the search data in the
name
query parameter should require the database to do a case-insensitive search for text fields- eg, searching for ‘ring’ should find ‘Turing’ and ‘Ring World’
- for merchants, the user can send
- allow the user to send one or more price-related query parameters, applicable to items only:
min_price=4.99
should look for anything with a price equal to or greater than $4.99max_price=99.99
should look for anything with a price less than or equal to $99.99- both
min_price
andmax_price
can be sent
- for items, the user will send EITHER the
name
parameter OR either/both of theprice
parameters- users should get an error if
name
and either/both of theprice
parameters are sent
- users should get an error if
Valid examples:
GET /api/v1/merchants/find_one?name=Mart
GET /api/v1/items/find_one?name=ring
GET /api/v1/items/find_one?min_price=50
GET /api/v1/items/find_one?max_price=150
GET /api/v1/items/find_one?max_price=150&min_price=50
Invalid examples:
GET /api/v1/<resource>/find_one
- parameter cannot be missing
GET /api/v1/<resource>/find_one?name=
- parameter cannot be empty
GET /api/v1/items/find_one?name=ring&min_price=50
- cannot send both
name
andmin_price
- cannot send both
GET /api/v1/items/find_one?name=ring&max_price=50
- cannot send both
name
andmax_price
- cannot send both
GET /api/v1/items/find_one?name=ring&min_price=50&max_price=250
- cannot send both
name
andmin_price
andmax_price
- cannot send both
Example JSON response for GET /api/v1/merchants/find?name=ring
{
"data": {
"id": 4,
"type": "merchant",
"attributes": {
"name": "Ring World"
}
}
}
“Find All” endpoints
These endpoints will follow the same rules as the “find_one” endpoints.
The JSON response will always be an array of objects, even if zero matches or only one match is found.
It should not return a 404 if no matches are found.
Example JSON response for GET /api/v1/merchants/find_all?name=ring
{
"data": [
{
"id": "4",
"type": "merchant",
"attributes": {
"name": "Ring World"
}
},
{
"id": "1",
"type": "merchant",
"attributes": {
"name": "Turing School"
}
}
]
}
Non-RESTful: Merchants with Most Revenue
This endpoint should return a variable number of merchants ranked by total revenue.
The URI should follow this pattern: GET /api/v1/merchants/most_revenue?quantity=x
where x
is the number of merchants to be returned. The quantity parameter is required, and should return an error if it is missing or if it is not an integer greater than 0.
Example JSON response for GET /api/v1/merchants/most_revenue?quantity=2
{
"data": [
{
"id": "1",
"type": "merchant_name_revenue",
"attributes": {
"name": "Turing School",
"revenue": 512.256128
}
},
{
"id": "4",
"type": "merchant_name_revenue",
"attributes": {
"name": "Ring World",
"revenue": 245.130001
}
}
]
}
Non-RESTful: Merchants with Most Items Sold
This endpoint should return a variable number of merchants ranked by total number of items sold:
The URI should follow this pattern: GET /api/v1/merchants/most_items?quantity=x
where x
is the number of merchants to be returned. The quantity should default to 5 if not provided, and return an error if it is not an integer greater than 0.
Example JSON response for GET /api/v1/merchants/most_items?quantity=2
{
"data": [
{
"id": "1",
"type": "items_sold",
"attributes": {
"name": "Turing School",
"count": 512
}
},
{
"id": "4",
"type": "items_sold",
"attributes": {
"name": "Ring World",
"count": 128
}
}
]
}
Non-RESTful: Revenue across Date Range
This endpoint should return the total revenue across all merchants between the given dates, inclusive of the start and end date.
The URI should follow this pattern: GET /api/v1/revenue?start_date=<start_date>&end_date=<end_date>
Assume your users will only send dates in the format YYYY-MM-DD. Revenue must be counted for any invoices on the end_date as well.
An error should be returned if either/both the start date or end date are not provided.
Example JSON response for GET /api/v1/revenue?start=2012-03-09&end=2012-03-24
{
"data": {
"id": null,
"attributes": {
"revenue" : 43201227.8000003
}
}
}
Non-RESTful: Total Revenue for a Given Merchant
This endpoint should return the total revenue for a single merchant.
The URI should follow this pattern: GET /api/v1/revenue/merchants/:id
Example JSON response for GET /api/v1/revenue/merchants/1
{
"data": {
"id": "42",
"type": "merchant_revenue",
"attributes": {
"revenue" : 532613.9800000001
}
}
}
Non-RESTful: Find items ranked by Revenue
The endpoint will return a quantity of items ranked by descending revenue.
The URI should follow this pattern: GET /api/v1/items/revenue?quantity=x
where ‘x’ is the maximum count of results to return.
- quantity should default to 10 if not provided
- endpoint should return an error if it is not an integer greater than 0.
Example JSON response for GET /api/v1/items/revenue?quantity=1
{
"data": [
{
"id": 4,
"type": "item_revenue",
"attributes": {
"name": "Men's Titanium Ring",
"description": "Fine titanium ring",
"unit_price": 299.99,
"merchant_id": 54,
"revenue": 19823.12985
}
}
]
}
Non-RESTful: Potential Revenue of Unshipped Orders, ranked by “potential” Revenue
Imagine that we want to build a report of the orders which have not yet shipped. How much money is being left on the table for these merchants if they just called Federal Package Logistics to come pick up the boxes…
The URI should follow this pattern: GET /api/v1/revenue/unshipped?quantity=x
where ‘x’ is the maximum count of results to return.
- quantity should default to 10 if not provided
- should return an error if it is not an integer greater than 0.
Example JSON response for GET /api/v1/revenue/unshipped?quantity=2
{
"data": [
{
"id": 834,
"type": "unshipped_order",
"attributes": {
"potential_revenue": 5923.78
}
},
{
"id": 28,
"type": "unshipped_order",
"attributes": {
"potential_revenue": 3298.63
}
}
]
}
Non-RESTful: Report by Month of Revenue Generated
We would like a full report of all revenue, sorted by week (the database can do this for you!). The dates you get back from PostgreSQL will represent the first day of the week
The URI should follow this pattern: GET /api/v1/revenue/weekly
Example JSON response for GET /api/v1/revenue/weekly
{
"data": [
{
"id": null,
"type": "weekly_revenue",
"attributes": {
"week": "2012-03-05",
"revenue": 14981117.170000013
}
},
{
"id": null,
"type": "weekly_revenue",
"attributes": {
"week": "2012-03-12",
"revenue": 18778641.380000062
}
},
{
"id": null,
"type": "weekly_revenue",
"attributes": {
"week": "2012-03-19",
"revenue": 19106531.87999994
}
},
{
"id": null,
"type": "weekly_revenue",
"attributes": {
"week": "2012-03-26",
"revenue": 4627284.439999996
}
}
]
}