/runcobo

An api framework with type-safe params, elegant json serializer. Thanks for enjoying it! 👻👻 https://runcobo.github.io/docs/

Primary LanguageCrystalMIT LicenseMIT

Runcobo

Crystal CI Built with Crystal Latest release API docs

Introduction

Runcobo is a general purpose framework built on Crystal.

Commands

runcobo [<commands>...] [<arguments>...]

  Commands:
    routes                      - Print all routes of the app.
    version                     - Print the current version of the runcobo.
    help                        - Print usage synopsis.

  Options:
    -h, --help                       Print usage synopsis.
    -v, --version                    Print the current version of the runcobo.

Project Architecture

lib/                # Library
src/
    main.cr         # Entry file
    actions/        # Actions Directory
        ...
    assets/         # Assets Directory
    views/          # Views Directory
        layouts/    # Layouts directory
        ...
    models/         # Models Directory
        ...
shards.yml          # The packages congfiuration file
shards.lock         # The lock file for packages congfiuration file

Keep in this architecture to use layouts macro, render macro.

Design Architecture

MVC (Model-View-Controller)

Design Principles

  • Simple Design must be simple, both in implementation and interface.
  • Intuitive Design must be intuitive.
  • Consistent Design must be consistent.

Installation

Install Crystal

Crystal is a language for humans and computers.

Crystal is a type safe, compiled language inspired by the simplicity of Ruby. Type safety means more errors are caught by the compiler during development, so you can be more confident about your code working in production.

See https://crystal-lang.org/install/ to install Crystal.

Install Runcobo

You can install Runcobo from sources. Other installations are working on.

curl -L https://github.com/runcobo/runcobo/archive/stable.zip --output runcobo.zip
unzip runcobo.zip
cd runcobo-stable/
sudo make install
runcobo -v

Getting Started

1.Init a Crystal project.

crystal init app demo && cd demo

2.Add the dependency to your shard.yml and run shards install.

dependencies:
  runcobo:
    github: runcobo/runcobo

3.Write down the following code in src/demo.cr.

require "runcobo"

class Api::V1::Add < BaseAction
  get "/api/v1/add"
  query NamedTuple(a: Int32, b: Int32)

  call do
    sum = params[:a] + params[:b]
    render_plain sum.to_s
  end
end

Runcobo.start

4.Run server.

crystal src/demo.cr

5.Send request.

curl "http://0.0.0.0:3000/api/v1/add?a=1&b=2"

6.Auto restart server.

# Use nodemon to watch file changed and auto restart server.
sudo npm install -g nodemon
nodemon -e "cr,water,jbuilder,yml" --exec "crystal run" src/demo.cr

Route

Runcobo declares routes in every Actions. If you access the route, it will run into related Action.

In this way, you can know the routes of current Action quickly.

You can declare RESTful routes as you want or not.

In my personal view, RESTful is too abstact when you were far way from a CRUD system like Backend Management System.

When writing API, I believe urls should be named like functions, not objects or resources.

When writing a Backend Management System, enjoy RESTful.

Routes Declaration

Runcobo declares routes by following methods: get, post, put, patch, delete, options, head. An Action can bind to one or more routes.

class Example < BaseAction
  get "/books"
  get "/books/:id"
  post "/books"
  put "/books/:id"
  patch "/books/:id"
  delete "/books/:id"
  options "/books/:id"
  head "/books/:id"

  call do
    render_plain "Hello World"
  end
end

URL Params

URL Params can be declared in the route like /add/:apple_count/:banana_count. And then you should use url method to declare the type of URL params.

class Example < BaseAction
  get "/add/:apple_count/:banana_count"
  url NamedTuple(apple_count: Int32, banana_count: Int32)

  call do
    sum = params[:apple_count] + params[:banana_count]
    render_plain sum.to_s
  end
end

Custom HTTP method

If you need a route with custom HTTP method, such as LINK, UNLINK, FIND or PURGE, then you can use route method to declare it.

class Example < BaseAction
  route "LINK", "/books/:id"

  call do
    render_plain "Hello World"
  end
end

Action

Runcobo use one action one file.

Before Filter

class BeforeExample < BaseAction
  before required_login
  def required_login
    Runcobo::Log.info { "Required Login" }
  end

  get "/before_example"
  call do
    render_plain "Hello World!"
  end
end

After Filter

class AfterExample < BaseAction
  after log_params
  def log_params
    Runcobo::Log.info { "#{params}" }
  end

  get "/after_example"
  call do
    render_plain "Hello World!"
  end
end

Skip Filter

class BaseAction
  before required_login
  def required_login
    Runcobo::Log.info { "Required Login" }
  end
end

class SkipExample < BaseAction
  skip required_login

  get "/skip_example"
  call do
    render_plain "Hello World!"
  end
end

Params

Type-safe Params

Params in Runcobo are type-safe.

Three Steps To Use Params

  • First, declare what params you expect and what type you expect by following methods: url, query, form and json.
  • Next, Runcobo parses request and wraps params to a local variable called params.
  • Third, use params variable to get or set request params.

Url Params

class UrlExample < BaseAction
  get "/url_example/:a/:b"
  url NamedTuple(a: Int32, b: Int32)

  call do
    sum = params[:a] + params[:b]
    render_plain sum.to_s
  end
end

Query Params

class QueryExample < BaseAction
  get "/query_example"
  query NamedTuple(a: Int32, b: Int32)

  call do
    sum = params[:a] + params[:b]
    render_plain sum.to_s
  end
end

Form Params

class FormExample < BaseAction
  post "/form_example"
  form NamedTuple(a: Int32, b: Int32)

  call do
    sum = params[:a] + params[:b]
    render_plain sum.to_s
  end
end

JSON Params

class JsonExample < BaseAction
  post "/json_example"
  json NamedTuple(a: Int32, b: Int32)

  call do
    sum = params[:a] + params[:b]
    render_plain sum.to_s
  end
end

Params Merge Order

You can declare various kinds of params in a action. If params are in same key, they will be merged in following order:

Query Params < Form Params < JSON Params < Url Params

Render

Render HTML

class WaterExample < BaseAction
  get "/water_example"
  call do
    render_water "examples/index"
  end
end

Render Plain

class PlainExample < BaseAction
  get "/plain_example"
  call do
    render_plain "Hello World!"
  end
end

Render Body

class BodyExample < BaseAction
  get "/body_example"
  call do
    render_body "Hello World!"
  end
end

Render JSON

class JbuilderExample < BaseAction
  get "/jbuilder_example"
  call do
    render_jbuilder "examples/index"
  end
end

View

Runcobo renders JSON by Jbuilder, renders HTML by Water. Jbuilder is a template engine designed for json using plain Crystal. Water is a template engine designed for html using plain Crystal.

Data transfer

All methods or variables defined in the action are available in the views. This is because the views are compiled in the same scope as the action.

Layout

You can override the default layout conventions in your actions by using the layout declaration. For example:

class BaseAction
  layout "application"
  #...
end

Partial

Runcobo renders partial view by build-in read_file macro. There's no magic about partial view. For example,

src/views/books/index.jbuilder

json.array! "books", books do |json, book|
  {{ read_file("src/views/books/_base_book.jbuilder").id }}
end

src/views/books/_base_book.jbuilder

json.book_id      book.id
json.author       book.author
json.name         book.name
json.published_at book.published_at

Render JSON

src/controllers/books/index.cr

class Books::Index < BaseAction
  get "/books"
  call do
    books = Book.all
    render_jbuilder "books/index"
  end
end

src/views/books/index.jbuilder

json.array! "books", books do |json, book|
  json.book_id      book.id
  json.author       book.author
  json.name         book.name
  json.published_at book.published_at
end

Then, output a JSON string.

{
  "books": [{
    "book_id": 1,
    "author": "David",
    "name": "Crystal Programming",
    "published_at": "2020-08-08T20:00:00+00:00"
  }]
}

Render Partial

src/views/books/index.jbuilder

json.array! "books", books do |json, book|
  {{ read_file("src/views/books/_base_book.jbuilder").id }}
end

src/views/books/_base_book.jbuilder

json.book_id      book.id
json.author       book.author
json.name         book.name
json.published_at book.published_at

Render HTML

src/controllers/books/index.cr

class Books::Index < BaseAction
  get "/books"
  call do
    books = Book.all
    render_water "books/index"
  end
end

src/views/books/index.water

table %|class="table table-hover"| {
  thead {
    tr {
      th "ID"
      th "Author"
      th "Name"
      th "Published At"
    } 
  }
  tbody {
    books.each do |book|
      tr {
        td book.id
        td book.author
        td book.name
        td book.published_at
      }
    end
  }
}

Then, output a HTML string.

<table class="table table-hover">
  <thead>
    <tr>
      <th>ID</th>
      <th>Author</th>
      <th>Name</th>
      <th>Published At</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>David</td>
      <td>Crystal Programming</td>
      <td>2020-08-02 14:07:41 +08:00</td>
    </tr>
  </tbody>
</table>

Render Partial

src/views/books/index.jbuilder

{{ read_file("src/views/books/table.water").id }}

src/views/books/table.water

table %|class="table table-hover"| {
  thead {
    tr {
      th "ID"
      th "Author"
      th "Name"
      th "Published At"
    } 
  }
  tbody {
    books.each do |book|
      tr {
        td book.id
        td book.author
        td book.name
        td book.published_at
      }
    end
  }
}

Model

You can see all ORMs from [awesome-crystal].(https://github.com/veelenga/awesome-crystal#ormodm-extensions)

Runcobo prefers to use jennifer.cr , you can check its docs and api.

jennifer.cr is an Active Record pattern implementation with flexible query chainable builder and migration system.

Contributing

  1. Fork it (https://github.com/runcobo/runcobo/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Write and execute specs (crystal spec) and formatting checks (crystal tool format)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a new Pull Request

Contributors