pax_global_header00006660000000000000000000000064134037720640014520gustar00rootroot0000000000000052 comment=70cad2b2bdf9a80fd5fe16d6adfa261c4caab61e ruby-snorlax-0.1.8/000077500000000000000000000000001340377206400141735ustar00rootroot00000000000000ruby-snorlax-0.1.8/.gitignore000066400000000000000000000001351340377206400161620ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.gem ruby-snorlax-0.1.8/.travis.yml000066400000000000000000000001141340377206400163000ustar00rootroot00000000000000language: ruby rvm: - 2.2.2 before_install: gem install bundler -v 1.10.5 ruby-snorlax-0.1.8/CODE_OF_CONDUCT.md000066400000000000000000000026321340377206400167750ustar00rootroot00000000000000# Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) ruby-snorlax-0.1.8/Gemfile000066400000000000000000000004601340377206400154660ustar00rootroot00000000000000source 'https://rubygems.org' # Specify your gem's dependencies in restful_controller.gemspec gemspec gem 'rails', '> 4.1.0' group :development, :test do gem 'byebug', require: nil gem 'temping' gem 'mocha' gem 'sqlite3' end group :test do gem 'codeclimate-test-reporter', require: nil end ruby-snorlax-0.1.8/LICENSE.txt000066400000000000000000000020671340377206400160230ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 James Kiesel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-snorlax-0.1.8/README.md000066400000000000000000000170741340377206400154630ustar00rootroot00000000000000# Snorlax ![](http://img3.wikia.nocookie.net/__cb20140924022259/pokemon/images/9/9f/143Snorlax_OS_anime.png) Snorlax is an opinionated, flexible, and well-RESTed gem for building Rails APIs. It's designed to Do The Right Thing™ by default, but allow you to customize where necessary. It's been extracted from [Loomio](www.github.com/loomio/loomio). ## Installation Add this line to your application's Gemfile: ```ruby gem 'snorlax' ``` And then execute: $ bundle Or install it yourself as: $ gem install snorlax ## Usage There are two primary ways to use Snorlax. The easier way is to inherit from it: ``` class MyController < Snorlax::Base end ``` But, if you're already inheriting from something else, Snorlax makes it easy to simply apply itself to your controller without being a parent, by calling `snorlax_used_rest!` like so: ``` class MyController < SomeOtherController snorlax_used_rest! end ``` Once you've got it installed, you're good to go! #### Some things to know about Snorlax: Snorlax is designed to provide a set of flexible API endpoints for client side consumption. It uses side loading, which means that all of the records returned are unnested and easily available for consumption. #### The show action By default, Snorlax will fetch the requested model by `id` using the `ActiveModel::Base.find` method. To override this behaviour, you may override the `load_resource` method in your controller. ``` class TaurosController < Snorlax::Base def load_resource SafariZone.find(params[:id]) end end ``` NB: This would be a great place to ensure that the current user is allowed to view the requested resource. #### The index action Snorlax DEMANDS that you define two separate methods for the index action to work properly: - `public_records` - records which are available publicly (ie, they can be viewed even if current_user is nil) - `visible_records` - records which the current user is allowed to access. ``` class StaryusController < Snorlax::Base def public_records Staryu.visible_to_public end def visible_records current_user.staryus end end ``` If these are the same, you may override the `accessible_records` method instead ``` class StarmiesController < Snorlax::Base def accessible_records Starmie.all end end ``` The default index action look like this: ``` def index instantiate_collection respond_with_collection end ``` but there are many options to customize it. ##### Paging the index action A Snorlax controller can accept `from` and `per` parameters for paging. For example, ``` /api/vi/psyducks?from=0&per=100 ``` will return the first 100 psyducks within the accessible records. If no `from` or `per` params are provided, Snorlax will default to the first 50 records. (from = 0, per = 50) You can override the default page size by overriding the `default_page_size` method. ``` class WobbuffetsController < Snorlax::Base def default_page_size 25 end end ``` ##### Timeframing the index action A Snorlax controller can accept `since` and `until` parameters for timeframing. The date it looks for defaults to `created_at`. For example, ``` /api/v1/charmanders?since=11-11-2011&until=12-12-2012 ``` will return charmanders which were created between Nov 11, 2011, and Dec 12, 2012. You can override the datetime column the query looks at by passing the `timeframe_for` option. For example, ``` /api/v1/charmeleons?since=11-11-2011&until=12-12-2012&timeframe_for=evolved_at ``` will return charmeleons which evolved between Nov 11, 2011, and Dec 12, 2012 Since and until can be in any format that `Date.parse` will accept. ##### Overriding the timeframing or pagination options If you want to disallow timeframing and/or pagination for a particular action, simple pass `timeframe_collection: false` or `page_collection: false` when the collection is instantiated. ``` class SquirtlesController < Snorlax::Base def all_squirtles instantiate_collection page_collection: false, timeframe_collection: false respond_with_collection end end ``` ##### Custom filtering on the index action Snorlax also provides a dead simple way to provide your own filters, in addition to and in combination with the ones provided. You can do this by passing a block to the `instantiate_collection` method in your controller action. For example, ``` class ChanseysController < Snorlax::Base def sleeping instantiate_collection { |collection| collection.where(sleeping: true) } respond_with_collection end end ``` Will return all sleeping Chanseys. (This can be combined with timeframing and paginating as well.) (NB: this filtering happens before the collection is paginated or timeframed, so it's a good opportunity to add additional filters or apply an order to your records. Or both!) ##### Customizing the serializer options By default, Snorlax will find a serializer based on the controller name, and serialize out your collection of records with a root. ##### To override serializer and root for all actions in the controller You can override the serializer being used or the serializer root across the controller by defining a `resource_serializer` or `serializer_root` method, respectively: ``` class DittosController < Snorlax::Base def resource_serializer PokemonSerializer end def serializer_root :pokemon end end ``` This will result is JSON like this: ``` { pokemon: [{ pokemon_serializer_field_a: 'valueA', pokemon_serializer_field_b: 'valueB' }] } ``` ##### To override serializer and root for a single action To do this for a particular action, pass the `serializer` or `root` option to respond_with_collection. (NB: You can also pass a scope to the serializer in this way, by passing a 'scope' option) ``` class DittosController < Snorlax::Base def index instantiate_collection respond_with_collection serializer: PokemonSerializer, root: :pokemon, scope: { is_ditto: true } end end ``` #### The create / update actions By default, Snorlax will load the requested resource, perform an action on it, and then respond with the resource (with the same response as the show action) It looks like this: ``` def create instantiate_resource create_action respond_with_resource end def create_action resource.save end ``` ##### Overriding the action command If you have more complicated logic for creating or updating records (your creation / update logic is wrapped up in a service object, for example), simply override the `create_action` method (or the `update_action` method in the case of update) ``` def MewtwosController < Snorlax::Base def create_action Science.faff_about(resource_params) end end ``` (NB: this is a good opportunity to make sure that the current user has permission to modify the record.) ##### Overriding the resource serialization `respond_with_resource` accepts the same parameters as respond_with_collection, to allow for single-action customization of the json response. ``` def MewtwosController < Snorlax::Base def create instantiate_resource create_action respond_with_resource { serializer: LegendarySerializer, root: 'legendary_pokemon' } end end ``` ##### If there are errors on the object TODO: explain the respond_with_errors behaviour ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/gdpelican/snorlax. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). ruby-snorlax-0.1.8/Rakefile000066400000000000000000000003341340377206400156400ustar00rootroot00000000000000require 'bundler/gem_tasks' require 'rake/testtask' task default: :test Rake::TestTask.new :test do |t| t.libs << 'lib' t.libs << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = true t.warning = false end ruby-snorlax-0.1.8/app/000077500000000000000000000000001340377206400147535ustar00rootroot00000000000000ruby-snorlax-0.1.8/app/controllers/000077500000000000000000000000001340377206400173215ustar00rootroot00000000000000ruby-snorlax-0.1.8/app/controllers/snorlax/000077500000000000000000000000001340377206400210075ustar00rootroot00000000000000ruby-snorlax-0.1.8/app/controllers/snorlax/base.rb000066400000000000000000000117621340377206400222550ustar00rootroot00000000000000module Snorlax class Base < ::ApplicationController def self.snorlax_used_rest!(controller) controller.class_eval do if defined? CanCan::AccessDenied rescue_from(CanCan::AccessDenied) { |e| respond_with_standard_error e, 403 } end if defined? Pundit::NotAuthorizedError rescue_from(Pundit::NotAuthorizedError) { |e| respond_with_standard_error e, 403 } end rescue_from(ActionController::UnpermittedParameters) { |e| respond_with_standard_error e, 400 } rescue_from(ActionController::ParameterMissing) { |e| respond_with_standard_error e, 400 } rescue_from(ActiveRecord::RecordNotFound) { |e| respond_with_standard_error e, 404 } def show respond_with_resource end def index instantiate_collection respond_with_collection end def create instantiate_resource create_action respond_with_resource end def create_action resource.save end def update load_resource update_action respond_with_resource end def update_action resource.update(resource_params) end def destroy load_resource destroy_action destroy_response end private def collection instance_variable_get :"@#{resource_name.pluralize}" end def resource instance_variable_get :"@#{resource_name}" end def resource=(value) instance_variable_set :"@#{resource_name}", value end def collection=(value) instance_variable_set :"@#{resource_name.pluralize}", value end def instantiate_resource self.resource = resource_class.new(resource_params) end def instantiate_collection(timeframe_collection: true, page_collection: true) collection = accessible_records collection = yield collection if block_given? collection = timeframe_collection collection if timeframe_collection collection = page_collection collection if page_collection self.collection = collection.to_a end def timeframe_collection(collection) if resource_class.try(:has_timeframe?) && (params[:since] || params[:until]) parse_date_parameters # I feel like Rails should do this for me.. collection.within(params[:since], params[:until], params[:timeframe_for]) else collection end end def parse_date_parameters %w(since until).each { |field| params[field] = DateTime.parse(params[field].to_s) if params[field] } end def page_collection(collection) collection.offset(params[:from].to_i).limit((params[:per] || default_page_size).to_i) end def accessible_records if current_user.is_logged_in? visible_records else public_records end end def visible_records raise NotImplementedError.new end def public_records raise NotImplementedError.new end def default_page_size 50 end def destroy_action resource.destroy end def destroy_response render json: {success: 'success'} end def load_resource self.resource = resource_class.find(params[:id]) end def resource_params permitted_params.send resource_name end def resource_symbol resource_name.to_sym end def resource_name controller_name.singularize end def resource_class resource_name.camelize.constantize end def resource_serializer "#{resource_name}_serializer".camelize.constantize end def respond_with_resource(scope: default_scope, serializer: resource_serializer, root: serializer_root) if resource.errors.empty? respond_with_collection(resources: [resource], scope: scope, serializer: serializer, root: root) else respond_with_errors end end def respond_with_collection(resources: collection, scope: default_scope, serializer: resource_serializer, root: serializer_root) render json: resources, scope: scope, each_serializer: serializer, root: root end def respond_with_standard_error(error, status) render json: {exception: error.class.to_s}, root: false, status: status end def respond_with_errors render json: {errors: resource.errors.as_json}, root: false, status: 422 end def serializer_root controller_name end def default_scope {} end end end snorlax_used_rest!(self) end end ruby-snorlax-0.1.8/bin/000077500000000000000000000000001340377206400147435ustar00rootroot00000000000000ruby-snorlax-0.1.8/bin/console000077500000000000000000000005271340377206400163370ustar00rootroot00000000000000#!/usr/bin/env ruby require "bundler/setup" require "restful_controller" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start ruby-snorlax-0.1.8/bin/setup000077500000000000000000000001631340377206400160310ustar00rootroot00000000000000#!/bin/bash set -euo pipefail IFS=$'\n\t' bundle install # Do any other automated setup that you need to do here ruby-snorlax-0.1.8/lib/000077500000000000000000000000001340377206400147415ustar00rootroot00000000000000ruby-snorlax-0.1.8/lib/snorlax.rb000066400000000000000000000005661340377206400167630ustar00rootroot00000000000000require 'snorlax/version' require 'snorlax/engine' module Snorlax end class ActionController::Base # Method to allow controllers to apply Snorlax to themselves # (g@nked from Inherited resources: https://github.com/josevalim/inherited_resources/blob/master/lib/inherited_resources.rb) def self.snorlax_used_rest! Snorlax::Base.snorlax_used_rest!(self) end end ruby-snorlax-0.1.8/lib/snorlax/000077500000000000000000000000001340377206400164275ustar00rootroot00000000000000ruby-snorlax-0.1.8/lib/snorlax/engine.rb000066400000000000000000000000701340377206400202160ustar00rootroot00000000000000module Snorlax class Engine < Rails::Engine end end ruby-snorlax-0.1.8/lib/snorlax/version.rb000066400000000000000000000000471340377206400204420ustar00rootroot00000000000000module Snorlax VERSION = "0.1.8" end ruby-snorlax-0.1.8/snorlax.gemspec000066400000000000000000000017521340377206400172330ustar00rootroot00000000000000# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'snorlax/version' Gem::Specification.new do |spec| spec.name = "snorlax" spec.version = Snorlax::VERSION spec.authors = ["James Kiesel (gdpelican)"] spec.email = ["james@loomio.org"] spec.summary = "Snorlax is an opinionated, but flexible and well-RESTed controller for Rails APIs" spec.homepage = "http://www.github.com/gdpelican/snorlax" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_runtime_dependency "rails", "> 4.1" spec.add_development_dependency "bundler", "~> 1.10" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "minitest", "~> 5.7" end