diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index cbe5f8c6..80b3b122 100755 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -9,17 +9,14 @@ jobs: deploy_docs: runs-on: ubuntu-latest container: - image: crystallang/crystal:latest-alpine + image: crystallang/crystal steps: - uses: actions/checkout@v2 - - name: Install Dependencies - run: shards install --production - name: Build - run: make docs + run: crystal docs - name: Deploy - uses: JamesIves/github-pages-deploy-action@3.5.2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: JamesIves/github-pages-deploy-action@2.0.1 + env: + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BRANCH: gh-pages FOLDER: docs - SINGLE_COMMIT: true diff --git a/README.md b/README.md index e247cdc2..652df5ec 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,6 @@ A web framework comprised of reusable, independent components. -## Installation - -1. Add the dependency to your `shard.yml`: - -```yaml -dependencies: - athena: - github: athena-framework/athena -``` - -2. Run `shards install` - ## Documentation Everything is documented in the [API Docs](https://athena-framework.github.io/athena/Athena/Routing.html). diff --git a/shard.yml b/shard.yml index 51d1ba34..659dc7e0 100755 --- a/shard.yml +++ b/shard.yml @@ -1,6 +1,6 @@ name: athena -version: 0.8.0 +version: 0.9.0 crystal: 0.35.0 @@ -25,7 +25,7 @@ dependencies: version: ~> 0.1.0 athena-dependency_injection: github: athena-framework/dependency-injection - branch: di-refactor-v2 + version: ~> 0.2.0 amber_router: github: amberframework/amber-router version: ~> 0.4.1 diff --git a/spec/redirect_response_spec.cr b/spec/redirect_response_spec.cr index 8f2eb41e..298b8536 100644 --- a/spec/redirect_response_spec.cr +++ b/spec/redirect_response_spec.cr @@ -7,7 +7,7 @@ describe ART::RedirectResponse do end it "disallows non redirect codes" do - expect_raises(ArgumentError, "422 is not an HTTP redirect status code.") do + expect_raises(ArgumentError, "'422' is not an HTTP redirect status code.") do ART::RedirectResponse.new("addresss", 422) end end diff --git a/src/annotations.cr b/src/annotations.cr index 8a944727..46e1b977 100644 --- a/src/annotations.cr +++ b/src/annotations.cr @@ -1,5 +1,5 @@ module Athena::Routing - # Defines a GET endpoint. + # Defines a `GET` endpoint. # # A corresponding `HEAD` endpoint is also defined. # @@ -15,7 +15,7 @@ module Athena::Routing # ``` annotation Get; end - # Defines a POST endpoint. + # Defines a `POST` endpoint. # # ## Fields # * path : `String` - The path for the endpoint, may also be provided as the first positional argument. @@ -29,7 +29,7 @@ module Athena::Routing # ``` annotation Post; end - # Defines a PUT endpoint. + # Defines a `PUT` endpoint. # # ## Fields # * path : `String` - The path for the endpoint, may also be provided as the first positional argument. @@ -43,7 +43,7 @@ module Athena::Routing # ``` annotation Put; end - # Defines a PATCH endpoint. + # Defines a `PATCH` endpoint. # # ## Fields # * path : `String` - The path for the endpoint, may also be provided as the first positional argument. @@ -57,7 +57,7 @@ module Athena::Routing # ``` annotation Patch; end - # Defines a DELETE endpoint. + # Defines a `DELETE` endpoint. # # ## Fields # * path : `String` - The path for the endpoint, may also be provided as the first positional argument. @@ -71,7 +71,7 @@ module Athena::Routing # ``` annotation Delete; end - # Applies an `ART::ParamConverterInterface` to a given parameter. + # Applies an `ART::ParamConverterInterface` to a given argument. # # See `ART::ParamConverterInterface` for more information on defining a param converter. # @@ -91,7 +91,7 @@ module Athena::Routing # ``` annotation ParamConverter; end - # Defines a `ART::Parameters::QueryParameter` tied to a given route. + # Defines a query parameter tied to a given argument. # # The type of the query param is derived from the type restriction of the associated controller action argument. # @@ -107,8 +107,8 @@ module Athena::Routing # # ``` # @[ART::Get(path: "/example")] - # @[ART::QueryParam(name: "value")] - # def get_user(name : String) : Nil + # @[ART::QueryParam("query_param")] + # def get_user(query_param : String) : Nil # end # ``` annotation QueryParam; end diff --git a/src/athena.cr b/src/athena.cr index 97cc6662..2519887b 100755 --- a/src/athena.cr +++ b/src/athena.cr @@ -36,36 +36,318 @@ require "./ext/request" # Convenience alias to make referencing `Athena::Routing` types easier. alias ART = Athena::Routing -# Athena's Routing component, `ART` for short, provides an event based framework for converting a request into a response -# and includes various abstractions/useful types to make that process easier. -# -# Athena is an event based framework; meaning it emits `ART::Events` that are acted upon to handle the request. -# Athena also utilizes `Athena::DependencyInjection` to provide a service container layer. The service container layer -# allows a project to share/inject useful objects between various types, such as a custom `AED::EventListenerInterface`, `ART::Controller`, or `ART::ParamConverterInterface`. -# See the corresponding types for more information. -# -# * See `ART::Controller` for documentation on defining controllers/route actions. -# * See `ART::Config` for documentation on configuration options available for the Routing component. -# * See `ART::Events` for documentation on the events that can be listened on during the request's life-cycle. -# * See `ART::ParamConverterInterface` for documentation on using param converters. -# * See `ART::Exceptions` for documentation on exception handling. +# Athena is a set of independent, reusable [components](https://github.com/athena-framework) with the goal of providing +# a set of high quality, flexible, and robust framework building blocks. These components could be used on their own, +# or outside of the Athena ecosystem, to prevent every framework/project from needing to "reinvent the wheel." +# +# The `Athena::Routing` component is the result of combining these components into a single robust, flexible, and self-contained framework. +# +# ## Getting Started +# +# Athena does not have any other dependencies outside of `Crystal`/`Shards`. +# It is designed in such a way to be non-intrusive, and not require a strict organizational convention in regards to how a project is setup; +# this allows it to use a minimal amount of setup boilerplate while not preventing it for more complex projects. +# +# ### Installation +# +# Add the dependency to your `shard.yml`: +# +# ```yaml +# dependencies: +# athena: +# github: athena-framework/athena +# version: 0.9.0 +# ``` +# +# Run `shards install`. This will install Athena and its required dependencies. +# +# ### Usage +# +# Athena has a goal of being easy to start using for simple use cases, while still allowing flexibility/customizability for larger more complex use cases. +# +# #### Routing +# +# Athena is a MVC based framework, as such, the logic to handle a given route is defined in an `ART::Controller` class. +# +# ``` +# require "athena" +# +# # Define a controller +# class ExampleController < ART::Controller +# # Define an action to handle the related route +# @[ART::Get("/")] +# def index : String +# "Hello World" +# end +# +# # The macro DSL can also be used +# get "/" do +# "Hello World" +# end +# end +# +# # Run the server +# ART.run +# +# # GET / # => Hello World +# ``` +# Annotations applied to the methods are used to define the HTTP method this method handles, such as `ART::Get` or `ART::Post`. A macro DSL also exists to make them a bit less verbose; +# `ART::Controller.get` or `ART::Controller.post`. +# +# Controllers are simply classes and routes are simply methods. Controllers and actions can be documented/tested as you would any Crystal class/method. +# +# #### Route Parameters +# +# Parameters, such as path/query parameters, are also defined via annotations and map directly to the method's arguments. +# +# ``` +# require "athena" +# +# class ExampleController < ART::Controller +# @[ART::QueryParam("negative")] +# @[ART::Get("/add/:value1/:value2")] +# def add(value1 : Int32, value2 : Int32, negative : Bool = false) : Int32 +# sum = value1 + value2 +# negative ? -sum : sum +# end +# end +# +# ART.run +# +# # GET /add/2/3 # => 5 +# # GET /add/5/5?negative=true # => -10 +# # GET /add/foo/12 # => {"code":422,"message":"Required parameter 'value1' with value 'foo' could not be converted into a valid 'Int32'"} +# ``` +# +# Arguments are converted to their expected types if possible, otherwise an error response is automatically returned. +# The values are provided directly as method arguments, thus preventing the need for `env.params.url["name"]` and any boilerplate related to it. +# Just like normal methods arguments, default values can be defined. The method's return type adds some type safety to ensure the expected value is being returned. +# +# An `ART::Response` can also be used in order to fully customize the response, such as returning a specific status code, adding some one-off headers, or saving memory by directly +# writing the response value to the Response IO. +# +# ``` +# require "athena" +# require "mime" +# +# class ExampleController < ART::Controller +# # A GET endpoint returning an `ART::Response`. +# @[ART::Get(path: "/css")] +# def css : ART::Response +# ART::Response.new ".some_class { color: blue; }", headers: HTTP::Headers{"content-type" => MIME.from_extension(".css")} +# end +# end +# +# ART.run +# +# # GET /css # => ".some_class { color: blue; }" +# ``` +# +# An `ART::Events::View` is emitted if the returned value is _NOT_ an `ART::Response`. By default, non `ART::Response`s are JSON serialized. +# However, this event can be listened on to customize how the value is serialized. +# +# #### Error Handling +# +# Exception handling in Athena is similar to exception handling in any Crystal program, with the addition of a new unique exception type, `ART::Exceptions::HTTPException`. +# Custom `HTTP` errors can also be defined by inheriting from `ART::Exceptions::HTTPException` or a child type. +# A use case for this could be allowing additional data/context to be included within the exception. +# +# Non `ART::Exceptions::HTTPException` exceptions are represented as a `500 Internal Server Error`. +# +# When an exception is raised, Athena emits the `ART::Events::Exception` event to allow an opportunity for it to be handled. +# By default these exceptions will return a `JSON` serialized version of the exception, via `ART::ErrorRenderer`, that includes the message and code; with the proper response status set. +# If the exception goes unhandled, i.e. no listener set an `ART::Response` on the event, then the request is finished and the exception is reraised. +# +# ``` +# require "athena" +# +# class ExampleController < ART::Controller +# get "divide/:num1/:num2", num1 : Int32, num2 : Int32, return_type: Int32 do +# num1 // num2 +# end +# +# get "divide_rescued/:num1/:num2", num1 : Int32, num2 : Int32, return_type: Int32 do +# num1 // num2 +# # Rescue a non `ART::Exceptions::HTTPException` +# rescue ex : DivisionByZeroError +# # in order to raise an `ART::Exceptions::HTTPException` to provide a better error message to the client. +# raise ART::Exceptions::BadRequest.new "Invalid num2: Cannot divide by zero" +# end +# end +# +# ART.run +# +# # GET /divide/10/0 # => {"code":500,"message":"Internal Server Error"} +# # GET /divide_rescued/10/0 # => {"code":400,"message":"Invalid num2: Cannot divide by zero"} +# ``` +# +# ### Advanced Usage +# +# Athena also ships with some more advanced features to provide more flexibility/control for an application. +# These features may not be required for a simple application; however as the application grows they may become more useful. +# +# #### Param Converters +# `ART::ParamConverterInterface`s allow complex types to be supplied to an action via its arguments. +# An example of this could be extracting the id from `/users/10`, doing a DB query to lookup the user with the PK of `10`, then providing the full user object to the action. +# Param converters abstract any custom parameter handling that would otherwise have to be done in each action. +# +# ``` +# require "athena" +# +# @[ADI::Register] +# struct MultiplyConverter < ART::ParamConverterInterface +# # :inherit: +# def apply(request : HTTP::Request, configuration : Configuration) : Nil +# arg_name = configuration.name +# +# return unless request.attributes.has? arg_name +# +# value = request.attributes.get arg_name, Int32 +# request.attributes.set arg_name, value * 2, Int32 +# end +# end +# +# class ParamConverterController < ART::Controller +# @[ART::Get(path: "/multiply/:num")] +# @[ART::ParamConverter("num", converter: MultiplyConverter)] +# def multiply(num : Int32) : Int32 +# num +# end +# end +# +# ART.run +# +# # GET / multiply/3 # => 6 +# ``` +# +# #### Middleware +# +# Athena is an event based framework; meaning it emits `ART::Events` that are acted upon internally to handle the request. +# These same events can also be listened on by custom listeners, via `AED::EventListenerInterface`, in order to tap into the life-cycle of the request. +# An example use case of this could be: adding common headers, cookies, compressing the response, authentication, or even returning a response early like `ART::Listeners::CORS`. +# +# ``` +# require "athena" +# +# @[ADI::Register] +# struct CustomListener +# include AED::EventListenerInterface +# +# # Specify that we want to listen on the `Response` event. +# # The value of the hash represents this listener's priority; +# # the higher the value the sooner it gets executed. +# def self.subscribed_events : AED::SubscribedEvents +# AED::SubscribedEvents{ +# ART::Events::Response => 25, +# } +# end +# +# def call(event : ART::Events::Response, dispatcher : AED::EventDispatcherInterface) : Nil +# event.response.headers["FOO"] = "BAR" +# end +# end +# +# class ExampleController < ART::Controller +# get "/" do +# "Hello World" +# end +# end +# +# ART.run +# +# # GET / # => Hello World (with `FOO => BAR` header) +# ``` +# +# #### Dependency Injection +# +# Athena utilizes `Athena::DependencyInjection` to provide a service container layer. +# DI allows controllers/other services to be decoupled from specific implementations. +# This makes testing easier as test implementations of the dependencies can be used. +# +# In Athena, most everything is a service that belongs to the container, which is unique to the current request. The major benefit of this is it allows various types to be shared amongst the services. +# For example, allowing param converters, controllers, etc. to have access to the current request via the `ART::RequestStore` service. +# +# Another example would be defining a service to store a `UUID` to represent the current request, then using this service to include the UUID in the response headers. +# +# ``` +# require "athena" +# require "uuid" +# +# @[ADI::Register] +# struct RequestIDStore +# HEADER_NAME = "X-Request-ID" +# +# # Inject `ART::RequestStore` in order to have access to the current request's headers. +# def initialize(@request_store : ART::RequestStore); end +# +# property request_id : String? = nil do +# # Check the request store for a request. +# request = @request_store.request? +# +# # If there is a request and it has the Header, +# if request && request.headers.has_key? HEADER_NAME +# # use that ID. +# request.headers[HEADER_NAME] +# else +# # otherwise generate a new one. +# UUID.random.to_s +# end +# end +# end +# +# @[ADI::Register] +# struct RequestIDListener +# include AED::EventListenerInterface +# +# def self.subscribed_events : AED::SubscribedEvents +# AED::SubscribedEvents{ +# ART::Events::Response => 0, +# } +# end +# +# def initialize(@request_id_store : RequestIDStore); end +# +# def call(event : ART::Events::Response, dispatcher : AED::EventDispatcherInterface) : Nil +# # Set the request ID as a response header +# event.response.headers[RequestIDStore::HEADER_NAME] = @request_id_store.request_id +# end +# end +# +# class ExampleController < ART::Controller +# get "/" do +# "" +# end +# end +# +# ART.run +# +# # GET / # => (`X-Request-ID => 07bda224-fb1d-4b82-b26c-19d46305c7bc` header) +# ``` +# +# The main benefit of having `RequestIDStore` and not doing `event.response.headers[RequestIDStore::HEADER_NAME] = UUID.random.to_s` directly is that the value could be used in other places. +# Say for example you have a route that enqueues messages to be processed asynchronously. The `RequestIDStore` could be inject into that controller/service in order to include the same `UUID` +# within the message in order to expand tracing to async contexts. Without DI, like in other frameworks, there would not be an easy to way to share the same instance of an object between +# different types. It also wouldn't be easy to have access to data outside the request context. +# +# DI is also what "wires" everything together. For example, say there is an external shard that defines a listener. All that would be required to use that listener is install and require the shard, +# DI takes care of the rest. This is much easier and more flexible than needing to update code to add a new `HTTP::Handler` instance to an array. module Athena::Routing protected class_getter route_resolver : ART::RouteResolver { ART::RouteResolver.new } # The `AED::Event` that are emitted via `Athena::EventDispatcher` to handle a request during its life-cycle. - # Athena adds a `HTTP::Request#attributes` getter that returns a `Hash(String, Bool | Int32 | String | Float64 | Nil)` which can be used to store simple information that can be used later. + # Custom events can also be defined and dispatched within a controller, listener, or some other service. # # See each specific event for more detailed information. module Athena::Routing::Events; end # Exception handling in Athena is similar to exception handling in any Crystal program, with the addition of a new unique exception type, `ART::Exceptions::HTTPException`. # - # When an exception is raised, Athena emits the `ART::Events::Exception` event to allow an opportunity for it to be handled. If the exception goes unhanded, i.e. no listener set + # When an exception is raised, Athena emits the `ART::Events::Exception` event to allow an opportunity for it to be handled. If the exception goes unhandled, i.e. no listener set # an `ART::Response` on the event, then the request is finished and the exception is reraised. Otherwise, that response is returned, setting the status and merging the headers on the exceptions # if it is an `ART::Exceptions::HTTPException`. See `ART::Listeners::Error` and `ART::ErrorRendererInterface` for more information on how exceptions are handled by default. # # To provide the best response to the client, non `ART::Exceptions::HTTPException` should be rescued and converted into a corresponding `ART::Exceptions::HTTPException`. - # Custom HTTP errors can also be defined by inheriting from `ART::Exceptions::HTTPException`. A use case for this could be allowing for additional data/context to be included + # Custom HTTP errors can also be defined by inheriting from `ART::Exceptions::HTTPException` or a child type. A use case for this could be allowing for additional data/context to be included # within the exception that ultimately could be used in a `ART::Events::Exception` listener. module Athena::Routing::Exceptions; end @@ -115,7 +397,7 @@ module Athena::Routing # An `Array(ART::Arguments::ArgumentMetadata)` that `self` requires. getter arguments : ArgumentsType - # An `Array(ART::ParamConverterInterface::ConfigurationInterface)` representing the `ART::ParamConverter`s applied to `self. + # An `Array(ART::ParamConverterInterface::ConfigurationInterface)` representing the `ART::ParamConverter`s applied to `self`. getter param_converters : Array(ART::ParamConverterInterface::ConfigurationInterface) def initialize( @@ -141,7 +423,7 @@ module Athena::Routing Controller end - # Executes `#action` with the provided *arguments* array. + # Executes the action related to `self` with the provided *arguments* array. def execute(arguments : Array) : ReturnType @action.call.call *{{ArgTypeTuple.type_vars.empty? ? "Tuple.new".id : ArgTypeTuple}}.from arguments end @@ -150,6 +432,8 @@ module Athena::Routing # Runs an `HTTP::Server` listening on the given *port* and *host*. # # ``` + # require "athena" + # # class ExampleController < ART::Controller # @[ART::Get("/")] # def root : String @@ -160,15 +444,15 @@ module Athena::Routing # ART.run # ``` # See `ART::Controller` for more information on defining controllers/route actions. - def self.run(port : Int32 = 3000, host : String = "0.0.0.0", ssl : OpenSSL::SSL::Context::Server | Bool | Nil = nil, reuse_port : Bool = false) - ART::Server.new(port, host, ssl, reuse_port).start + def self.run(port : Int32 = 3000, host : String = "0.0.0.0", reuse_port : Bool = false) : Nil + ART::Server.new(port, host, reuse_port).start end # :nodoc: # # Currently an implementation detail. In the future could be exposed to allow having separate "groups" of controllers that a `Server` instance handles. struct Server - def initialize(@port : Int32 = 3000, @host : String = "0.0.0.0", @ssl : OpenSSL::SSL::Context::Server | Bool | Nil = nil, @reuse_port : Bool = false) + def initialize(@port : Int32 = 3000, @host : String = "0.0.0.0", @reuse_port : Bool = false) # Define the server @server = HTTP::Server.new do |context| # Handle the request diff --git a/src/controller.cr b/src/controller.cr index 5e14ddbd..80417041 100644 --- a/src/controller.cr +++ b/src/controller.cr @@ -3,7 +3,7 @@ # # Additional annotations also exist for setting a query param or a param converter. See `ART::QueryParam` and `ART::ParamConverter` respectively. # -# Child controllers must inherit from `ART::Controller` (or an abstract child of it). Each request gets its own instance of the controller to better allow for DI via `Athena::DI`. +# Child controllers must inherit from `ART::Controller` (or an abstract child of it). Each request gets its own instance of the controller to better allow for DI via `Athena::DependencyInjection`. # # A route action can either return an `ART::Response`, or some other type. If an `ART::Response` is returned, then it is used directly. Otherwise an `ART::Events::View` is emitted to convert # the action result into an `ART::Response`. By default, `ART::Listeners::View` will JSON encode the value if it is not handled earlier by another listener. @@ -20,9 +20,9 @@ # @[ART::Prefix("athena")] # class TestController < ART::Controller # # A GET endpoint returning an `ART::Response`. -# @[ART::Get(path: "/css")] +# @[ART::Get("/css")] # def css : ART::Response -# ART::Response.new ".some_class { color: blue; }", 200, HTTP::Headers{"content-type" => MIME.from_extension(".css")} +# ART::Response.new ".some_class { color: blue; }", headers: HTTP::Headers{"content-type" => MIME.from_extension(".css")} # end # # # A GET endpoint using a param converter to render a template. @@ -32,8 +32,8 @@ # # # user.ecr # # Morning, <%= user.name %> it is currently <%= time %>. # # ``` -# @[ART::ParamConverter(param: "user", converter: SomeConverter(User))] -# @[ART::Get(path: "/wakeup/:id")] +# @[ART::ParamConverter("user", converter: SomeConverter)] +# @[ART::Get("/wakeup/:id")] # def wakeup(user : User) : ART::Response # # Template variables not supplied in the action's arguments must be defined manually # time = Time.utc @@ -45,7 +45,7 @@ # # A GET endpoint with no params returning a `String`. # # # # Action return type restrictions are required. -# @[ART::Get(path: "/me")] +# @[ART::Get("/me")] # def get_me : String # "Jim" # end @@ -53,7 +53,7 @@ # # A GET endpoint with no params returning `Nil`. # # `Nil` return types are returned with a status # # of 204 no content -# @[ART::Get(path: "/no_content")] +# @[ART::Get("/no_content")] # def get_no_content : Nil # # Do stuff # end @@ -62,7 +62,7 @@ # # # # The parameters of a route _MUST_ match the arguments of the action. # # Type restrictions on action arguments are required. -# @[ART::Get(path: "/add/:val1/:val2")] +# @[ART::Get("/add/:val1/:val2")] # def add(val1 : Int32, val2 : Int32) : Int32 # val1 + val2 # end @@ -71,7 +71,7 @@ # # # # A non-nilable type denotes it as required. If the parameter is not supplied, and no default value is assigned, an `ART::Exceptions::BadRequest` exception is raised. # @[ART::QueryParam("time", constraints: /\d:\d:\d/)] -# @[ART::Get(path: "/event/:event_name/")] +# @[ART::Get("/event/:event_name/")] # def event_time(event_name : String, time : String) : String # "#{event_name} occurred at #{time}" # end @@ -80,13 +80,13 @@ # # # # A nilable type denotes it as optional. If the parameter is not supplied (or could not be converted), and no default value is assigned, it is `nil`. # @[ART::QueryParam("user_id")] -# @[ART::Get(path: "/events/(:page)")] +# @[ART::Get("/events/(:page)")] # def events(user_id : Int32?, page : Int32 = 1) : NamedTuple(user_id: Int32?, page: Int32) # {user_id: user_id, page: page} # end # # # A GET endpoint with param constraints. The param must match the supplied Regex or it will not match and return a 404 error. -# @[ART::Get(path: "/time/:time/", constraints: {"time" => /\d{2}:\d{2}:\d{2}/})] +# @[ART::Get("/time/:time/", constraints: {"time" => /\d{2}:\d{2}:\d{2}/})] # def get_constraint(time : String) : String # time # end @@ -95,27 +95,25 @@ # # # # It is recommended to use param converters to pass an actual object representing the data (assuming the body is JSON) # # to the route's action; however the raw request body can be accessed by typing an action argument as `HTTP::Request`. -# @[ART::Post(path: "/test/:expected")] +# @[ART::Post("/test/:expected")] # def post_body(expected : String, request : HTTP::Request) : Bool # expected == request.body.try &.gets_to_end # end # end # -# spawn ART.run -# -# CLIENT = HTTP::Client.new "localhost", 3000 -# -# CLIENT.get("/athena/css").body # => .some_class { color: blue; } -# CLIENT.get("/athena/wakeup/17").body # => Morning, Allison it is currently 2020-02-01 18:38:12 UTC. -# CLIENT.get("/athena/me").body # => "Jim" -# CLIENT.get("/athena/add/50/25").body # => 75 -# CLIENT.get("/athena/event/foobar?time=1:1:1").body # => "foobar occurred at 1:1:1" -# CLIENT.get("/athena/events").body # => {"user_id":null,"page":1} -# CLIENT.get("/athena/events/17?user_id=19").body # => {"user_id":19,"page":17} -# CLIENT.get("/athena/time/12:45:30").body # => "12:45:30" -# CLIENT.get("/athena/time/12:aa:30").body # => 404 not found -# CLIENT.get("/athena/no_content").body # => 204 no content -# CLIENT.post("/athena/test/foo", body: "foo").body # => true +# ART.run +# +# # GET /athena/css" # => .some_class { color: blue; } +# # GET /athena/wakeup/17" # => Morning, Allison it is currently 2020-02-01 18:38:12 UTC. +# # GET /athena/me" # => "Jim" +# # GET /athena/add/50/25" # => 75 +# # GET /athena/event/foobar?time=1:1:1" # => "foobar occurred at 1:1:1" +# # GET /athena/events" # => {"user_id":null,"page":1} +# # GET /athena/events/17?user_id=19" # => {"user_id":19,"page":17} +# # GET /athena/time/12:45:30" # => "12:45:30" +# # GET /athena/time/12:aa:30" # => 404 not found +# # GET /athena/no_content" # => 204 no content +# # POST /athena/test/foo", body: "foo" # => true # ``` abstract class Athena::Routing::Controller # Renders a template. @@ -138,10 +136,9 @@ abstract class Athena::Routing::Controller # end # end # - # spawn ART.run + # ART.run # - # CLIENT = HTTP::Client.new "localhost", 3000 - # CLIENT.get("/Fred").body # => Greetings, Fred! + # # GET /Fred # => Greetings, Fred! # ``` macro render(template) Athena::Routing::Response.new(headers: HTTP::Headers{"content-type" => "text/html"}) do |io| @@ -165,10 +162,9 @@ abstract class Athena::Routing::Controller # end # end # - # spawn ART.run + # ART.run # - # CLIENT = HTTP::Client.new "localhost", 3000 - # CLIENT.get("/Fred").body # =>