Skip to content

Instantly share code, notes, and snippets.

@blaix
Last active August 29, 2015 14:17
Show Gist options
  • Save blaix/10464939aa9712887a1b to your computer and use it in GitHub Desktop.
Save blaix/10464939aa9712887a1b to your computer and use it in GitHub Desktop.
A ruby web-dev framework that favors lots of small objects and dependency injection over DSLs and magic methods.
require "wren/application"
# The simplest Wren application has a route and a handler:
class HelloApp
extend Wren::Routing # provides get/put/post/etc methods
extend Wren::Application # provides handle and mount methods
# and a rack-compatible call method
handle get("/hello/:name"), with: -> (name:, request:, response:) {
response.text("Hello #{name}!")
}
end
# Wren apps are rack compatible. In config.ru:
run HelloApp.new
# A handler can be any object that responds to `call` and returns a rack
# compatible response (or an object to hand to a responder, but we'll get to
# that later). This makes it very easy to decouple your application logic from
# the application framework:
module Hello
class SayHello
def call(name:, request:, response:)
response.text("Hello #{name}!")
end
end
class Application
extend Wren::Routing
extend Wren::Application
handle get("/hello/:name"), with: SayHello.new
end
end
# It looks weird that a SayHello class is concerned with request/response.
# Let's refactor our app to use a responder, which is a special type of handler
# that has an additional call paramter to accept whatever object s returned by
# the handler in front of it.
module Hello
class SayHello
def call(name:, **)
"Hello #{name}!"
end
end
class RespondWithText
def call(text, response:, **)
response.text(text)
end
end
class Application
extend Wren::Routing
extend Wren::Application
handle get("/hello/:name"), with: SayHello.new,
then: RespondWithText.new
end
end
# Now we have small, highly-focused objects that can be wired together by the
# Application class. Your handlers can be unit tested in isolation, and your
# integration tests can hit the application object. Wren encourages this type
# of design and provides other features to support it.
# Let's go full-on MVC and add a model and a view:
require "wren/views/delegate"
module Hello
Person = Struct.new(:name)
class SayHello
def call(name:, response:, **)
person = Person.new(name)
# Wren's delegate view is a simple delegator with a render method that
# forwards template variables as messages to the passed-in object:
view = Wren::Views::Delegate.new(person)
# Views render templates with themselves as the context.
# So if the template looks like this: <h1>{{name}}</h1>
# then {{name}} will be replaced with `view.name` which in this case
# delegates to `person.name`.
#
# Any class can be a view if it includes Wren::Renderable.
#
# Wren values separation of concerns and testability so it prefers to
# keep templates free of logic by parsing them with Mustache. The plan is
# to allow other engines to be used using an adapter.
# `response.render` will call `view.render(template_name)` and respond
# with the result and the appropriate content type:
response.render(view, "say_hello")
# Template files will be searched for in `Application#template_dir`,
# which defaults to the directory of the application class +
# '/templates'. It will look for a file extension that matches the
# request format.
#
# So if the request was for HTML, the above is equivalent to:
# response.html(view.render("./templates/say_hello.html"))
# Or if the request was for JSON:
# response.json(view.render("./templates/say_hello.json"))
#
# `response.format` is available if you need to perform your own logic
# based on the request format.
end
end
class Application
# ...
end
end
# What if we want to say hello to other things with a name? As it stands now,
# SayHello depends on Person, so it can only say hello to people. We can invert
# that dependency (the D in SOLID) by passing in the person to the SayHello
# constructor. We can do the same for the view. Wren expects that you will
# design your code this way and uses this dependency injection pattern to
# enable useful default handlers for things like CRUD on a resource (more on
# that later).
module Hello
Person = Struct.new(:name)
class SayHello
attr_reader :model, :view
def initialize(model:, view:)
@model = model
@view = view
end
def call(name:, response:, **)
obj = model.new(name)
view = view.new(obj)
response.render(view, "say_hello")
end
end
class Application
extend Wren::Routing
extend Wren::Application
say_hello = SayHello.new(model: Person, view: Wren::Views::DelegateView)
handle get("/hello/:name"), with: say_hello
# With the dependencies inverted, SayHello could handle a request for
# anything that has a name:
# handle get("/hello/:name"), with: SayHello.new(model: Person, view: view)
# handle get("/dog/:name"), with: SayHello.new(model: Dog, view: view)
#
# This is how Wren provides things like Wren::Handlers::CreateResource
# without requiring special DSLs for configuring what model is used, etc.
#
# The general best practice is to let the outter layer of your application
# (usually the Application class) provide concrete dependencies and let
# your inner classes declare their needed dependencies as required, named
# constructor arguments. This lets you easily integration test your full
# application stack, and unit test your business logic in isolation
# (you can provide mock objects to meet needed dependencies).
end
end
# So far we've only been dealing with one route. What if you have more, and
# you want them to link to each other? To reduce duplication of routing
# knowledge, assign your routes to constants that can be accessed elsewhere.
# For example, imagine we live in a world where we want to say "Hello" *and*
# "Goodbye":
module MyApp
module Routes
extend Wren::Routing
SayHello = get("/hello/:name")
SayGoodbye = get("/goodbye/:name")
end
module Views
class DelegateView < Wren::Views::DelegateView
def hello_path
Routes::SayHello.path(name: source.name)
end
def goodbye_path
Routes::SayGoodbye.path(name: source.name)
end
end
end
class Application
extend Wren::Application
say_hello = Wren::Handlers::RenderTemplate.new(
template: "hello", view: Views::DelegateView)
say_goodbye = Wren::Handlers::RenderTemplate.new(
template: "goodbye", view: Views::DelegateView)
handle Routes::SayHello, with: say_hello
handle Routes::SayGoodbye, with: say_goodbye
end
end
# Here, our routes are completely decoupled from their handlers. This
# may seem extreme, but I like it. Routes sole responsibility are
# representing paths in our project. The application class is responsible
# for mapping those routes to handlers.
#
# There are no global routing helpers.
# If you need a piece of the system to understand paths, include or
# inject the routes.
#
# Since our templates are not allowed to contain logic (not even function
# calls like SayHello.path(model)), we need to define some path attributes.
# This may seem overly verbose, but I like the explicitness. Your template doesn't have
# anything available that you are not explicitly providing for it. I
# feel that this keeps your templates much cleaner and everything easier
# to understand and test.
# Our templates will probably look something like this:
#
# hello.html:
# <h1>Hello {{name}}!</h1>
# <a href="{{goodbye_path}}">Say goodbye</a>
#
# goodbye.html:
# <h1>Goodbye {{name}}!</h1>
# <a href="{{hello_path}}">Come back</a>
# The RenderTemplate handler in Wren looks like this:
class RenderTemplate
attr_reader :template, :view
def initialize(template:, view:)
@template = template
@view = view
end
def call(request:, response:, **context)
obj = OpenStruct.new(context)
view = view.new(obj)
response.render(template, view)
end
end
# One of the goals of Wren is to keep the framework's core code
# as a simple foundation but still help you hit the ground running by providing
# things like common RESTful Handler classes that are built on that
# foundation. But built the same way you would write your own handlers.
# This way the libraries and tests in wren itself can provide documentation
# and examples of how you can write your own wren code.
# TODO:
# * Figure out how routes know their mount path
# * Document a typical REST-oriented architecture
# * Document built-in Wren handlers for the default RESTful behavior.
# Routing ideas:
# * Make apps routable and provide a router method?
# * How to get routes all the way down to templates?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment