Coder Social home page Coder Social logo

sammyhenningsson / shaf Goto Github PK

View Code? Open in Web Editor NEW
19.0 0.0 0.0 1.19 MB

A framework for creating hypermedia driven APIs in Sinatra

Ruby 91.81% HTML 5.89% CSS 1.27% Shell 0.72% Dockerfile 0.15% JavaScript 0.17%
ruby rest rest-api framework hypermedia-api sinatra sequel hal-json

shaf's Introduction

Shaf (Sinatra Hypermedia API Framework)

Gem Version CI
Shaf is a framework for building hypermedia driven REST APIs. Its goal is to be like a lightweight version of rails new --api with hypermedia as a first class citizen. Instead of reinventing the wheel Shaf uses Sinatra and adds a layer of conventions similar to Rails. It uses Sequel as ORM and HALPresenter for policies and serialization (which means that the main mediatype is HAL).
Most APIs claiming to be RESTful completly lacks the concept of links and relies upon clients to construction urls to known endpoints. Thoses APIs are missing some of the concepts that Roy Fielding put together in is dissertation about REST.
If you don't have full understanding of what REST is then that's fine. Though you are encouraged to read up on the basics. Check out this blog for a great explanation of the building blocks of REST.
A short version is: REST was "invented" by describing how the web is architectured. Web components (e.g browsers, servers, cache proxies etc) all use the same interface, where URIs and mediatypes play a big part. This enables any browser to connect to any web server without prior knowledge about each other. An important part of this is to use hypermedia links, which makes it possible for components to evolve independently.

Building a REST API requires knowledge about standards and a lot of boring stuff. Shaf aims to reduce those prerequirements, minimize bikeshedding and to get you up and running quickly. Some of the benefits of using Shaf is that you get:

  • Scaffolding
  • Serialization
  • Authorization
  • Content negotiation
  • Documentation
  • Forms
  • Uri helpers
  • Pagination
  • Testing
  • HTTP caching
  • Link preloading (enables HTTP2 Push)

What's unique about Shaf?

The list above could be implemented in any web framework. So why use Shaf? Well, if you are comfortable writing it yourself, then perhaps Shaf might not be for you. However it can still be nice to have some conventions to rely upon, instead of having to decide all basic details. Like choosing a mediatype for instance (this is something people have very strong/different opinions about).
I don't think there's another Ruby web framework that emphasizes the principles of REST as much as Shaf does. An example of a unique feature (AFAIK) is that mediatypes are separated from controller actions. In most other frameworks, each controller action specifies the possible content type to be returned. In Shaf, controller actions returns Ruby objects. Depending on what clients want to receive (specified by the Accept header), the appropriate serializer is looked up and used to respond with the right representation. (This is not 100% true, since there's actually a helper, respond_with, that produces the usual [status_code, headers, response]. However you would still see it as an object being returned and serialization takes place afterwards. Parsing inputs is done using a similar approach.
The most unique feature is probably the usage of mediatype profiles, which are used both for machine readable definitions and for generating documentation.(See Mediatype profiles for more information.)

Getting started

Install Shaf with

gem install shaf

Then create a new project with shaf new followed by the name of the project. E.g.

shaf new blog

This will create a new directory with a bunch of files that make up the basics of a new API. Change into the this directory and install any missing depencencies.

cd blog
bundle

Your newly created project should contain the following files:

.
├── api
│   ├── controllers
│   │   ├── base_controller.rb
│   │   ├── docs_controller.rb
│   │   └── root_controller.rb
│   ├── policies
│   │   └── base_policy.rb
│   └── serializers
│       ├── base_serializer.rb
│       ├── documentation_serializer.rb
│       ├── error_serializer.rb
│       ├── form_serializer.rb
│       ├── root_serializer.rb
│       └── validation_error_serializer.rb
├── config
│   ├── bootstrap.rb
│   ├── customize.rb
│   ├── database.rb
│   ├── database.yml
│   ├── directories.rb
│   ├── helpers.rb
│   ├── initializers
│   │   ├── authentication.rb
│   │   ├── db_migrations.rb
│   │   ├── hal_presenter.rb
│   │   ├── logging.rb
│   │   └── sequel.rb
│   ├── initializers.rb
│   ├── paths.rb
│   └── settings.yml
├── config.ru
├── frontend
│   ├── assets
│   │   └── css
│   │       └── main.css
│   └── views
│       ├── form.erb
│       ├── headers.erb
│       ├── layout.erb
│       └── payload.erb
├── Gemfile
├── Gemfile.lock
├── Rakefile
└── spec
    ├── integration
    │   └── root_spec.rb
    ├── serializers
    │   └── root_serializer_spec.rb
    └── spec_helper.rb

You now have a functional API. Start the server with

shaf server

Then in another terminal run

curl localhost:3000/

Which should return the following payload.

{
  "_links": {
    "self": {
      "href": "http://localhost:3000/"
    }
  }
}

Hint: The output will actually not have any newlines and will look a bit more dense. To make the output more readable pipe the curl command to jq (which is a great a tool for dealing with json strings).

curl localhost:3000/ | jq

Or if you don't have jq installed, you can also pretty print json through Ruby. E.g:

curl localhost:3000/ | ruby -rjson -e "puts JSON.pretty_generate(JSON.parse(STDIN.read))"

The project also contains a few specs that you can run with rake

shaf test

Currently your API is pretty useless. Let's fix that by generating some scaffolding. The following command will create a new resource with two attributes (title and message).

shaf generate scaffold post title:string message:string 

This will output:

Added:      api/models/post.rb
Added:      db/migrations/20180224225335_create_posts_table.rb
Added:      api/serializers/post_serializer.rb
Added:      spec/serializers/post_serializer_spec.rb
Added:      api/policies/post_policy.rb
Added:      api/profiles/post.rb
Added:      api/forms/post_forms.rb
Added:      api/controllers/posts_controller.rb
Added:      spec/integration/posts_controller_spec.rb
Modified:   api/serializers/root_serializer.rb

As shown in the output, that command created, a model, a controller, a serializer and a policy. It also generated a DB migration file, some forms, some specs and a link to the new post collection was added the root resource. So let's check this out by migrating the DB and restarting the server. Close any running instance with Ctrl + C and then:

rake db:migrate
shaf server

Again in another terminal run

curl localhost:3000/ | jq

Which should now return the following payload.

{
  "_links": {
    "self": {
      "href": "http://localhost:3000/"
    },
    "posts": {
      "href": "http://localhost:3000/posts"
    }
  }
}

The root payload should now contain a link with rel posts. Lets follow that link..

curl localhost:3000/posts | jq

The response looks like this

{
  "_links": {
    "self": {
      "href": "http://localhost:3000/posts?page=1&per_page=25"
    },
    "up": {
      "href": "http://localhost:3000/"
    },
    "create-form": {
      "href": "http://localhost:3000/post/form"
    },
    "curies": [
      {
        "name": "doc",
        "href": "http://localhost:3000/doc/profiles/post{#rel}",
        "templated": true
      }
    ]
  },
  "_embedded": {
    "posts": []
  }
}

This is the collection of posts (which currently is empty, see $response['_embedded']['posts']). Notice the link with rel create-form. This is the api telling us that we may add new post resources. Let's follow that link!

curl http://localhost:3000/post/form | jq

The response looks like this

{
  "method": "POST",
  "name": "create-post",
  "title": "Create Post",
  "href": "http://localhost:3000/posts",
  "type": "application/json",
  "submit": "save",
  "_links": {
    "profile": {
      "href": "http://localhost:3000/doc/profiles/shaf-form"
    },
    "self": {
      "href": "http://localhost:3000/post/form"
    },
    "curies": [
      {
        "name": "doc",
        "href": "http://localhost:3000/doc/profiles/shaf-form{#rel}",
        "templated": true
      }
    ]
  },
  "fields": [
    {
      "name": "title",
      "type": "string"
    },
    {
      "name": "message",
      "type": "string"
    }
  ]
}

This form shows us how to create new post resources (see Forms for more info). A new post resource can be created with the following request

curl -H "Content-Type: application/json" \
     -d '{"title": "hello", "message": "lorem ipsum"}' \
     localhost:3000/posts | jq

The response shows us the new resource, with the attributes that we set as well as links for updating and deleting it.

{
  "title": "hello",
  "message": "lorem ipsum",
  "_links": {
    "profile": {
      "href": "http://localhost:3000/doc/profiles/post"
    },
    "collection": {
      "href": "http://localhost:3000/posts"
    },
    "self": {
      "href": "http://localhost:3000/posts/1"
    },
    "edit-form": {
      "href": "http://localhost:3000/posts/1/edit"
    },
    "doc:delete": {
      "href": "http://localhost:3000/posts/1"
    },
    "curies": [
      {
        "name": "doc",
        "href": "http://localhost:3000/doc/profiles/post{#rel}",
        "templated": true
      }
    ]
  }
}

This new resource is of course added to the collection of posts, which can now be retrieved by the link with rel collection.

curl localhost:3000/posts | jq

Response:

{
  "_links": {
    "self": {
      "href": "http://localhost:3000/posts?page=1&per_page=25"
    },
    "up": {
      "href": "http://localhost:3000/"
    },
    "create-form": {
      "href": "http://localhost:3000/post/form"
    },
    "curies": [
      {
        "name": "doc",
        "href": "http://localhost:3000/doc/profiles/post{#rel}",
        "templated": true
      }
    ]
  },
  "_embedded": {
    "posts": [
      {
        "title": "hello",
        "message": "lorem ipsum",
        "_links": {
          "profile": {
            "href": "http://localhost:3000/doc/profiles/post"
          },
          "collection": {
            "href": "http://localhost:3000/posts"
          },
          "self": {
            "href": "http://localhost:3000/posts/1"
          },
          "edit-form": {
            "href": "http://localhost:3000/posts/1/edit"
          },
          "doc:delete": {
            "href": "http://localhost:3000/posts/1"
          }
        }
      }
    ]
  }
}

Recap

We have built a very basic hypermedia driven API with only one type of resource. The neatest thing about this is that it only took four commands:

shaf new blog
bundle
shaf generate scaffold post title:string message:string 
rake db:migrate

Documentation

Contributing

If you find a bug or have suggestions for improvements, please create a new issue on Github. Pull request are welcome!

License

The gem is available as open source under the terms of the MIT License.

shaf's People

Contributors

dependabot[bot] avatar sammyhenningsson avatar

Stargazers

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

shaf's Issues

Upgrade to version 1.0.0 does not apply patches cleanly

VERBOSE=1 be shaf upgrade
Applying changes for version 0.6.0

Applying changes for version 1.0.0
patching file Rakefile
patching file config/initializers/db_migrations.rb
Hunk #1 FAILED at 2.
patch: **** malformed patch at line 44:  

Failed to apply patch for: config/initializers/db_migrations.rb
patching file config/initializers/sequel.rb
patching file config/initializers/logging.rb
patching file config/helpers.rb
patching file config/directories.rb
patching file config/database.rb
Hunk #2 FAILED at 16.
1 out of 2 hunks FAILED -- saving rejects to file config/database.rb.rej
Failed to apply patch for: config/database.rb
patching file config/bootstrap.rb
Hunk #1 succeeded at 1 with fuzz 1.
patching file api/controllers/base_controller.rb
patching file api/serializers/error_serializer.rb
patching file api/serializers/form_serializer.rb
patch: **** malformed patch at line 47:        end

Failed to apply patch for: api/serializers/form_serializer.rb
patching file config.ru

removing file: config/constants.rb

adding file: api/serializers/validation_error_serializer.rb
adding file: config/paths.rb

Applying changes for version 1.0.4
patching file api/controllers/base_controller.rb
Applying changes for version 1.1.0

Applying changes for version 1.1.1
patching file config/initializers/hal_presenter.rb
Hunk #1 succeeded at 1 with fuzz 1.
patching file config/bootstrap.rb.orig
Hunk #1 FAILED at 2.
1 out of 1 hunk FAILED -- saving rejects to file config/bootstrap.rb.orig.rej
Failed to apply patch for: config/bootstrap.rb.orig
patching file config/bootstrap.rb

Project is up-to-date! Shaf version: 1.1.1

Can't generate migrations

All migrations, except scaffold, are broken. Nothing is generated and the output shows wrong number of arguments (given 1, expected 0)

Upgrade to version 1.0.0 break settings

Upgrading Shaf version to 1.0.0 corrupts the config file and result in commands like shaf server and shaf console exit with a hard to understand failure.

The reason for this is that the key for the default settings(default: &default) is removed from the settings yaml file (config/settings.yml)

Workaround

This can be fixed by adding that line back to the config file. The config should look similar to this:

> head config/settings.yml 
---
default: &default
  port: 3000
  public_folder: frontend/assets
  views_folder: frontend/views
  documents_dir: doc/api
  migrations_dir: db/migrations
  fixtures_dir: spec/fixtures
  paginate_per_page: 25
  http_cache: on

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.