# Axel [](https://magnum.travis-ci.com/sportngin/axel) The building blocks for building a Sport Ngin Back-end API service: * add some error helpers (error object) * standardized XML/JSON envelope (metadata/error/result objects) * adding `suppress_response_codes` to params will force a 200 and will only show errors in the error object of the response body * standard controller responders to ensure JSON is always prefered, XML is acceptable, and the response always has a correct body that handles all errors ## Installation Add this line to your application's Gemfile: ```ruby source 'https://D9W5miiTyaf8EzrkWdTy@gem.fury.io/me/' # BELOW THE `source rubygems` gem 'axel' ``` Then execute: ```bash $ bundle ``` In your `app/controllers/application_controller.rb` (or wherever your base class is that will use these helpers) change to: ```ruby class ApplicationController < ActionController::Base include Axel::ControllerHelpers end ``` ## Usage ### Controller Specific Helpers #### Errors Errors object is available (as `errors`) from any controller: ```ruby errors.header_status # suppress_response_codes param will set to 200 by default errors << "Error message!" # add error messages errors.display # { status: errors.status_code, messages: errors.messages } errors.display? # Display errors? (was there an error added or status changed?) errors.messages errors.messages = [] errors.status = :not_found errors.status_code # => 404 errors.status = 403 errors.status_code # => 403 errors.new_error(status, *m) # set status, add an error or list of errors (not in an array object) # Can also set on the fly: rescue_error status: :not_found, message: "Error!" rescue_error status: :unproccessable_entity, messages: ["Error!", "Error2!"] ``` #### Metadata Metadata object that will be placed on every outgoing response body. You can add to the object like so: ```ruby metadata[:current_user] = current_user # the body will then set this on the outgoing body: # => { "metadata": { "current_user": "..." }, "result": "...", "...": "..." } ``` #### Responders At the top of each controller (for APIs) should be: ```ruby respond_to_json_xml ``` This defines `respond_to :json, :xml`. making these responders good to use for defaults: ```ruby respond_with_action :show # Good for the end of a create to show the created object render_action :show respond_to_empty # Render empty `result`, fill in `metadata`, `error` if necessary render_empty ``` #### Resource finder `find_resource` will automatically find a resource: ```ruby class PostsController < ApplicationController before_filter :find_resource def show respond_with end end curl http://localhost:3000/posts/1 # finds Post 1 and renders curl http://localhost:3000/posts/1 # => {"metadata":{},"error":{"status":404,"messages":["Record not found"]},"result":null} # can also customize the find_resource with `finder` and `value` # were `finder` is column and `value` is the value of the column ``` #### Param helpers ```ruby query_params # Only params on the query string post_params # POST params object_params # Either params under the object name (ie. {"user":".."} or all POST params object_name # singularized controller name for finding object_params ``` #### General workflow helpers Helpers: ```ruby force_ssl! # raises Errors::ForceSSL drop_meta! # we don't want the requester to get data like current_user, etc. drop_meta? # Did we call `drop_meta!`? format # The format passed from the request OR JSON render_nil_format # This is for rendering nils in json or XML (XML is blank, JSON is "null") safe_json_load # If you've already manually rendered some to json this helper safely loads it to a hash for re-JSONing ``` Errors are rescued to make for easier API workflow and responding: ```ruby Axel::Errors::ForceSSL # Drop meta # Status: Forbidden # Message: SSL is required ActiveRecord::RecordNotFound # Status: 404 # Message: Record not found Axel::Errors::NotAuthorized # Status: 401 # Message: User not authorized ActiveModel::MassAssignmentSecurity::Error # Status: 422 # Message: Unacceptable parameter being used ``` ### Interservice Helpers #### Some setup examples ```ruby Axel.config do |config| config.add_resource :user_service, :group, service: { url: "https://user-service.your-platform.com" } # Custom Path (otherwise defaults to plural of the resource_name (:user => "/users")) # config.add_resource :user_service, # :user, # service: { url: "https://user-service.your-platform.com" }, # attributes: [:user_name, :first_name], # path: "owner" end class Group < Axel::ServiceResource::Base # Let's say your class doesn't match the configured resource, you can: resource_name :group # Setup fields (gets accessors, all available in #attributes) field :name field :owner_id field :owner_type field :uri route "/groups/mine", :mine route "/user/:user_id/groups", :by_user_id # Attached to every Group request, you can define instance defaults as well def self.default_request_options { headers: { "Accepts" => "stuff!", Authorization: "Bearer #{some_access_token}" } } end end group = Group.new name: "test", owner_id: 1, owner_type: "Organization", uri: "blargh" group.save my_groups = Group.mine user_groups = Group.by_user_id(1) user_groups = Group.by_user_id(user_id: 1) a_group = Group.find(1) ``` #### Related data! ```ruby class User < Axel::ServiceResource::Base has_many :personas has_one :email_address, class: Email, included: true end class Email < Axel::ServiceResource::Base belongs_to :user, find_nested: true end class Persona < Axel::ServiceResource::Base belongs_to :user end u = User.find(1) u.personas # => API call to /users/1/personas puts data in an array of Persona objects u.email_address # => Uses `email_address` in the User data to put into Email objects Persona.find(1).user # => API call to /users/#{persona.user_id} fills User object Email.find(1).user # => API call to /personas/1/user fills User Object ``` #### Some Chainable Query methods ```ruby Group.where(name: "test") # => https://user-service.your-platform.com/groups?name=test Group.all params: { name: "test" }, body: jsonified_stuffs, headers: {}, method: :post Group.uri("https://user-service.dev/groups").where(name: "Jon").all(headers: {}) # => http://user-service.dev/groups?name=Jon Group.uri("https://user-service.dev").at_path("/other_groups_path").where name: "test" # => http://user-service.dev/other_groups_path?name=test Group.where(name: "Jon").all(headers: {}).none # => [] # Will always be empty array Group.none.where(name: "Jon") # => [] # can chain Group.without_default_path.at_path("groupies") # => https://user-service.your-platform.com/groupies # Enumerable works! Groups.where(name: "test").each { |g| puts e.attributes.inspect } groups = Groups.where(name: "test").to_a # drop cached JSON, then you can requery groups.reload.where(owner_id: 1) ``` #### More General Usage ``` ruby new_user.metadata # => { .. } # Metadata section of JSON output new_user.metadata[:current_user] # => { "id"=>1, "user_name"=>"admin", "first_name"=>"Happy", "last_name"=>"Gilmore", "uri"=>"http://user-service.dev/users/1"} new_user.errors.status_code # => 200 new_user.errors.messages # => [] new_user.result # => { ... } # Main envelope JSON body ``` For more info on what the requester is and can do check out [Axel::ServiceResource::Base][srb] ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request [srb]: http://github.com/sportngin/axel/tree/master/app/models/axel/service_resource/base.rb