Silicon
Silicon is a minimalistic web API framework. Its main idea is to implement an approach from Clean Architecture principles - "web is just a delivery mechanism". Silicon is build on top of IoC container Hypo and forces to use dependency injection approach everywhere.
Architecture

Separation of Concerns
Model-View-Controller (MVC) now is a sign of bad taste. Everybody who built more or less serious production web-application know that controllers become to be fat. Splitting controllers by context can a bit improve the situation but it's a visual trick, it still looks like:
class UserController
def index
# ...
end
def show
# ...
end
def update
# ...
end
# ...
end
It roughly violates one of main OOP rules - Single Responsibility Principle. Somebody can say "controller is only intended to control the process of request handling", but it's too large responsibility. Actually it's responsible for creating, updating and deleting models, showing views, services invocation and so on. And it's an ideal picture. Usual situation is when domain code is located in controllers. Silicon replaces regular controllers with a chain of atomic actions. It doesn't protect you to use bad practices but allows to easily separate [controller-]actions. Every action is a simple brick:
class ShowUsers
def call
# ...
end
end
class UpdateUser
def call
# ...
end
end
A route can represent much more complicated logic containing several steps. For that purpose Silicon provides an ability to chain actions:
-> update_user -> notify_admin -> log_action
Every of enumerated above actions are classes respond to method "call". More details about chains construction are
described below.
Router
Silicon Router is completely new vision of how to create flexible, lightweight, language-independent DSL for designing a schema of web-application routing. Rails, Hanami, Sinatra and others provide more or less visually similar DSL for defining routing schema. Their routers use Ruby blocks-based DSL for that. Ruby is a power, Ruby is a weakness: for such purpose Ruby is too verbose. An example demonstrating all features:
:receive
.->
/auth ->
:before -> load_current_user
/posts ->
GET -> list_posts
POST -> create_post
$id ->
:before -> load_post
GET -> show_post
PATCH -> update_post -> :respond =200
DELETE -> remove_post -> :respond =200
/comments ->
GET -> list_comments
POST => add_comment => notify_author =* notify_subscribers -> :respond <- comment_plain =201
$comment_id ->
GET -> show_comment -> :respond <- comment
DELETE -> remove_comment
:catch -> handle_errors
Routes configuration by default is located in file app/app.routes. You can change it's location in silicon.yml file.
Action path
Routes definition has tree-like structure. There're two root entries - :receive and :catch.
:receive section describes regular flow of incoming request.
:catch section defines an action which calls when an error raised somewhere in the regular flow.
:receive section should start from . - root point of the routing.
Every line of the definition is a piece of path to target action or chain.
Symbols -> and <TAB> emulates directory structure. Configuration demonstrated above can be interpreted like:
GET /auth/posts/$id (show_post)
DELETE /auth/posts/$id/comments/$comment_id (remove_comment)
Symbol $ allows to receive request parameters.
Action chaining
As you can see, some routes reference to a chain of actions, like:
-> update_user -> notify_admin -> log_action
It means that you can define a number of atomic operations in order to reach request goals.
-> is a simple sequential type of action. Process flow waits until finishing of it's execution before starting the next
action or making a response.
More interesting case:
=> add_comment => notify_author =* notify_subscribers
=> is a parallel operation. All parallel operations complete before the next sequential (->) or ending of the chain.
=* is an asynchronous operation. It must not be completed before next sequential and even ending of the chain.
The time of execution of asynchronous and parallel operations can be limited in config file (by default it's 10 seconds).
Sending Response
By default Silicon responds with HTTP status 200 (201 for POST) and empty body. You can define :respond instructions for sending custom status and specific response body:
...
POST => add_comment => ... -> :respond <- comment_plain =201
Expression :respond <- declares a view ("comment_plain") and a status = (201). Details about views formatting are described below.
Dependency Injection
Dependency Injection (DI) is a heart of Silicon web-application. Every Silicon action can utilize known variables/entities in the application. In example:
when you have:
GET /auth/posts/$id (show_post)
you can easily use request parameter:
class ShowPost
def initialize(id)
@id = id
end
def call
# somehow extract a post using @id
end
end
Every action returns a result that automatically registers in the container and can be used in further actions:
GET /auth/posts/$id (load_post, show_post)
class ShowPost
def initialize(load_post_result)
@post = load_post_result
end
# ...
end
You can customize the name of action result:
class LoadPost
#...
def result_name
'post'
end
end
and use:
class ShowPost
def initialize(post)
@post = post
end
# ...
end
In order to support another types by DI you need to adjust the configuration silicon.yml:
path:
dependencies:
- actions # default location
- services/injectable # additional dependencies location
Objects lifetime
As mentioned before, dependency injection is a heart of Silicon. And as you probably noticed we register dependencies for every new request. In order to avoid leaking the memory for request-specific objects Silicon uses Hypo::Scope lifetime style for registered objects. Every time when request is ending the dependencies remove from the container.
BTW, using Hypo::Scope and it's finalize method definition you can implement
Unit of Work pattern.
class DbSession
include Hypo::Scope
def initialize
@transaction = Transaction.new
end
def finalize
# unexpected behavior handling is in :catch section implementation
@transaction.commit
end
end
Views
By default Silicon handles only JSON requests using JBuilder engine. But you can extend a number of engines using
method add_view_builder in your app.rb:
class App < Silicon::App
def initialize
super
add_view_builder(MyHtmlViewBuilder, 'html')
end
end
View templates are located in views directory; you can change default location in silicon.yml:
path:
views: custom/views/location
In view template you can use any objects registered in the container. In example, you have a chain:
GET /posts/$id
-> load_user -> load_post -> load_comments -> :respond <- show_post
and it's implementation in Ruby:
class LoadPost
def initialize(id)
@id = id
end
def call
# Not a real ORM, just for the demo.
# Posts.find(id)
end
def result_name
'post'
end
end
class LoadComments
def initialize(id)
@id = id
end
def call
# Not a real ORM, just for the demo.
# Comments.where(post: post)
end
def result_name
# as you probably noticed this annoying action can be replaced
# with a convention in your own base class for application actions.
# Also instead of this declaration you can still use default name
# for actions like 'load_comments_result'.
'comments'
end
end
Draw the view:
json.post do
json.title @post.title
# ...
json.comments @comments do |comment|
json. comment.
# ...
end
end
Installation
Add this line to your application's Gemfile:
gem 'silicon'
And then execute:
$ bundle
Or install it yourself as:
$ gem install silicon
Getting Started
Development
Before making any contributions please make sure you are agree:
- 3 lines of code is better than 100 for the same functionality implementation, 0 lines is the best.
- Keep initial idea as simple as it possible. Plugins for additional functionality are more preferable.
- Do not use comments for obvious code; if your code is not obvious then try to make it obvious - extract method, variable, perform more steps to make it more clear.
Usual, but always helpful steps:
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/cylon-v/silicon.
License
The gem is available as open source under the terms of the MIT License.