Giter Site home page Giter Site logo

crepe's Introduction

Crepe Build Status Code Climate

Crepe is a lightweight API framework designed to help you write clean, fast web services in Ruby. With an elegant and intuitive DSL inspired by RSpec, and with a nod to Grape, Crepe makes API design simple.

Installation

In your application's Gemfile:

gem 'crepe', github: 'crepe/crepe'

If you're coming from Rails and/or you want a Crepe application with a thought-out file structure, you can use creperie to generate a new API:

$ gem install creperie --pre
$ crepe new my_api

Usage

Crepe APIs are, at their core, Rack applications. They can be created by subclassing Crepe::API. To detail Crepe's major features, we'll show how GitHub's Gist API could be written using Crepe:

# config.ru
require 'bundler/setup'
require 'crepe'

module Gist
  class API < Crepe::API
    # Like `let` in RSpec, this defines a lazy-loading helper.
    let(:current_user) { User.authorize!(request.headers['Authorization']) }

    namespace :gists do
      let(:gist_params) { params.slice(:files, :description, :public) }

      get do
        begin
          current_user.gists.limit(30)
        rescue User::Unauthorized
          Gist.limit(30)
        end
      end

      post { Gist.create(gist_params) }

      get(:public) { Gist.limit(30) }

      get(:starred) { current_user.starred_gists }

      # Specify a parameter as part of the namespace: /gists/:id/...
      param :id do
        let(:gist) { Gist.find(params[:id]) }

        get    { gist }
        put    { gist.update_attributes(gist_params) }
        patch  { gist.update_attributes(gist_params) }
        delete do
          if gist.user == current_user
            gist.destroy and head :no_content
          else
            error! :unauthorized
          end
        end

        # Specify a parameter constraint, e.g. a 40-character hex string
        param(sha: /\h{40}/) { gist.revisions.find(params[:sha]) }
        get(:commits) { gist.commits.limit(30) }

        get :star do
          if current_user.starred?(gist)
            head :no_content
          else
            head :not_found
          end
        end
        put(:star)    { current_user.star(gist) }
        delete(:star) { current_user.unstar(gist) }

        get(:forks)  { gist.forks.limit(30) }
        post(:forks) { current_user.fork(gist) }
      end
    end

    rescue_from ActiveRecord::RecordNotFound do |e|
      error! :not_found, e.message
    end

    rescue_from ActiveRecord::InvalidRecord do |e|
      error! :unprocessable_entity, e.message, errors: e.record.errors
    end

    rescue_from User::Unauthorized do |e|
      unauthorized! realm: 'Gist API'
    end
  end
end

run Gist::API

The above example will give you a Rack application that you can run with the rackup command, responding to the following routes:

GET    /gists
POST   /gists
GET    /gists/public
GET    /gists/starred
GET    /gists/:id
PUT    /gists/:id
PATCH  /gists/:id
DELETE /gists/:id
GET    /gists/:id/:sha
GET    /gists/:id/commits
GET    /gists/:id/star
PUT    /gists/:id/star
DELETE /gists/:id/star
GET    /gists/:id/forks
POST   /gists/:id/forks

Advanced usage

The above example only skims the surface of what Crepe can do. For more information, see the Crepe wiki.

License

(The MIT License.)

© 2013–2015 Stephen Celis [email protected], Evan Owen [email protected], David Celis [email protected]

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.

crepe's People

Contributors

cpursley avatar cygnan avatar davelyon avatar davidcelis avatar kainosnoema avatar stephencelis avatar tylersmalley avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

crepe's Issues

Pull parsing into modular Parser classes ĂĄ la Renderer

Parsing is currently handled by a single before filter that knows about a few hard-coded content types. We should really modularize parsing so that it can easily be extended with additional content types, similarly to how Renderers can be added for different response formats.

Where is Crepe slow?

We've got benchmarks thanks to @davidcelis, but it'd be nice to have a issues of the slowest places so other can contribute improvements.

Fix parsing

As it stands, Parser::Simple is, on its own, too simple.

While Endpoint splits responsibility between acceptance and rendering, Endpoint does no such thing between supported media types and parsing.

Why do we need granularity here? Well, the parser supports application/x-www-form-urlencoded and multipart/form-data by default. This seemed like a good idea at a time, but it actually makes Crepe a bit more difficult to step into because the first time you curl-debug, the params you expect to be parsed in from JSON will be nil unless you explicitly add -H "Content-Type: application/json". If it returned early with 415 Unsupported Media Type, it would be a lot easier to debug.

Renderer::Simple supports to_#{format} for any format by default, so we shouldn't need to get rid of form data support, but Renderer::Simple won't get hit at all if the format hasn't been whitelisted. We need the same ability to whitelist supported media types.

Support should be easy, but naming could be hard.

  • .respond_to handles acceptance, but will also shorthand-handle rendering with its **options of accepted formants to renderers.
  • .render handles renderers when Renderer::Simple is too simple.
  • .parse handles parsing.

How do things need to change?

One other thing to look into: do we properly convert application/vnd.name+json to application/json when parsing?

Handle versioning using Rack::MountSet instead of path mutation

Versioning is currently done by prepending the version to the path. This is restrictive and problematic for multiple reasons. It should be possible to specify versioning via path, query or header, and allow versions to be stacked so that Rack::MountSet can simply filter down through versions until finding one that matches the request.

#<NoMethodError: undefined method `include?' for #<Anchor "^">

This is the error:

Rack app error: #<NoMethodError: undefined method `include?' for #<Anchor "^">>
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:46:in `block in include?'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:10:in `block in each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:10:in `each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:10:in `each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:46:in `any?'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:46:in `include?'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/group.rb:65:in `include?'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:132:in `block (2 levels) in generate_split_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:132:in `each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:132:in `any?'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:132:in `block in generate_split_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:10:in `block in each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:10:in `each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/vendor/regin/regin/collection.rb:10:in `each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:98:in `each_with_index'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:98:in `generate_split_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:72:in `process_key'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:50:in `block (2 levels) in possible_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:49:in `each'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:49:in `inject'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:49:in `block in possible_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:48:in `map'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:48:in `possible_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/analysis/splitting.rb:59:in `report'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/route_set.rb:328:in `build_recognition_keys'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/route_set.rb:259:in `rehash'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/code_generation.rb:8:in `rehash'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/rack-mount-0.8.3/lib/rack/mount/route_set.rb:271:in `freeze'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:533:in `to_app'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:620:in `block in configured_routes'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:618:in `map'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:618:in `configured_routes'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:530:in `to_app'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:554:in `app'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/bundler/gems/crepe-9644652ee1f3/lib/crepe/api.rb:470:in `call'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/activerecord-4.1.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:621:in `call'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/activerecord-4.1.0/lib/active_record/query_cache.rb:36:in `call'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/rack_patch.rb:13:in `call'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/configuration.rb:71:in `call'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/server.rb:490:in `handle_request'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/server.rb:361:in `process_client'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/server.rb:254:in `block in run'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/thread_pool.rb:92:in `call'
/Users/krainboltgreene/.rvm/gems/ruby-2.1.1@touracle-api/gems/puma-2.8.2/lib/puma/thread_pool.rb:92:in `block in spawn_thread'

Example problem:

param(id: /^\d+$/) do
end

status(value) => ArgumentError: wrong number of arguments (0 for 1)

[10] pry(#<Crepe::Endpoint>)> status(1)
ArgumentError: wrong number of arguments (0 for 1)
from /Users/krainboltgreene/Code/endorsemint/api/lib/blankcrepe/account/router.rb:5:in `response'
  /Users/krainboltgreene/.rvm/gems/ruby-2.1.2@blankcrepe-api/bundler/gems/crepe-05f0e314de08/lib/crepe/endpoint.rb:127:in `status'
[12] pry(#<Crepe::Endpoint>)> status
ArgumentError: wrong number of arguments (0 for 1)
from /Users/krainboltgreene/Code/endorsemint/api/lib/blankcrepe/account/router.rb:5:in `response'
  /Users/krainboltgreene/.rvm/gems/ruby-2.1.2@blankcrepe-api/bundler/gems/crepe-05f0e314de08/lib/crepe/endpoint.rb:127:in `status'

I think the problem is here: https://github.com/crepe/crepe/blob/master/lib/crepe/endpoint.rb#L344

Flesh out content-negotiation/acceptance

We removed Middleware::ContentNegotiation since most of its functionality was replaced by a different way of routing versions, and Filter::Acceptance isn't really doing much anymore, but both had important functionality that should return.

Raise exceptions and rescue_from them

We call Endpoint#error! a lot of places where we should probably be raising custom exceptions with a default rescue_from. That way it becomes easier to override errors in your API if you want to format them differently.

Fix (or temporarily remove) versioning

I think there are 2 kinds of API versioning:

  1. Completely different versions where nothing is shared. E.g., Twitter API v1 and v1.1 (despite the lack of semantic versioning).
  2. APIs that gracefully introduce differences in the presentation of objects (new/renamed/removed properties) and the availability of endpoints. @kainosnoema mentioned an API that lets you pass in a unix timestamp that actually gives you the version of the API on that date.

Crepe really only supports no. 1 right now, and I argue that it should't!

Two completely different API versions should probably be run in completely different apps with some other server entity managing dispatch (either by rewriting the URL or managing content negotiation).

I'd love for Crepe to support no. 2, but that would require a lot and may not make it into 0.9.0 till we have a chance to truly flesh out requirements and, frankly, start using versioning ourselves.

Everything after my proc is slow

So I decided to test how my system would handle 10k records coming out. This is the (I think unsatisfactory) result:

22:29:42 web.1  | [76894] 127.0.0.1 - - [16/Sep/2014:22:29:42 -0500] "GET /accounts HTTP/1.1" 200 - 4.11

So I decided to investigate and this is my result:

22:29:39 web.1  | D, [2014-09-16T22:29:39.353423 #76894] DEBUG -- : BLOCK: 0.44155

My proc (AKA the block I passed to get) takes only 0.44155 seconds to resolve. The remaining 3.66845 seconds is after my process.

I tried to use oj, thinking it was serializing it with a slow JSON dumper, but I can't actively tell crepe to use my prefered serializer.

Any help?

Remove rack-mount

Rack::Mount only seems to offer better performance when you hit 100+ routes (in a single instance of Rack::Mount). Because Crepe APIs can be nested and composed, we should try to remove it in favor of our own router.

(Discussion based on #43.)

  • Write our own strexp compiler
  • Write our own router
  • routes.find { |r| path =~ r } is fast enough for < 100 routes, but we should also look into using a trie to speed up dispatch (as long as it doesn't make the base case much slower)

What, if anything, should be moved to CrĂȘperie?

Right now, CrĂȘperie's main purpose is as a CLI. However, it could also easily function as a convenience gem for CrĂȘpe and provide extra functionality for APIs that are more than basic. Here's a small list of things that I think could potentially be moved out of CrĂȘpe into CrĂȘperie, comprised of convenience methods that aren't necessarily used internally and could potentially not apply to smaller, lighter-weight APIs.

  • Crepe.env — this is actually used a couple times internally, put could be replaced with a check on ENV['RACK_ENV']. Then again, even basic APIs might use nicer environment checks.
  • Crepe.root — neither this method nor the ENV variable behind it are used internally. Basic APIs probably don't really need it, as they can be a single config.ru file and maybe a few other files.
  • Crepe::Helper::URLFor — could go either way. Could be useful even for basic APIs if they're doing Hypermedia.

Open to other ideas as well.

Out of date activesupport dependency :(

Bundler could not find compatible versions for gem "activesupport":
  In Gemfile:
    crepe (= 0.0.1.pre) ruby depends on
      activesupport (~> 3.2.0) ruby

    activerecord (~> 4.1) ruby depends on
      activesupport (4.1.0)

Release 0.9.0

This is a generic 0.9.0 ticket to track against the milestone of the same name.

Let's start a discussion here to figure out what's needed to get CrĂȘpe into a proper beta.

(/cc @davidcelis @kainosnoema)

Add more complex parameter validation

As it stands, we only support the Railsℱ-way:

let(:user_params) { params.require(:user).permit :email, :password }

This seems less useful to APIs, though I don't know if we need to remove the feature.

If we can support richer parameter validation, the API becomes more introspective and easier to generate documentation for.

https://github.com/mattt/sinatra-param is a good example, though verbose.

Caveats:

Potential naming confusion between the API.param method and Endpoint.#{param_validation} method.

Refine/document Endpoint as a protocol

The API DSL should completely work without the Endpoint class. We need to make sure that creating a completely custom Endpoint class is simple, which may change our current design.

Add support for JRuby

Looks like JRuby routing is completely busted at the moment: #65.

Not sure if rack-mount bug (see #67) or something else.

NameError: wrong constant name Blankcrepe::Account::Router_70130533228200

NameError: wrong constant name Blankcrepe::Account::Router_70130533228200
    /Users/krainboltgreene/.rvm/gems/ruby-2.1.2@blankcrepe-api/bundler/gems/crepe-ee059bb03885/lib/crepe/api.rb:468:in `const_set'
class Blankcrepe
  class Router < Crepe::API
    mount Blankcrepe::Account::Router => :accounts
  end
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.