# SoberSwag ![Ruby Test Status](https://github.com/SonderMindOrg/sober_swag/workflows/Ruby/badge.svg?branch=master) ![Linters Status](https://github.com/SonderMindOrg/sober_swag/workflows/Linters/badge.svg?branch=master) SoberSwag is a combination of [Dry-Types](https://dry-rb.org/gems/dry-types/1.2/) and [Swagger](https://swagger.io/) that makes your Rails APIs more awesome. Other tools generate documentation from a DSL. This generates documentation from *types*, which (conveniently) also lets you get supercharged strong-params-on-steroids. An introductory presentation is available [here](https://www.icloud.com/keynote/0bxP3Dn8ETNO0lpsSQSVfEL6Q#SoberSwagPresentation). ## Types for a fully-automated API SoberSwag lets you type your API using describe blocks. In any controller that includes `SoberSwag::Controller`, you get access to the super-cool DSL method `define`. This lets you type your API endpoint: ```ruby class PeopleController < ApplicationController include SoberSwag::Controller define :patch, :update, '/people/{id}' do query_params do attribute? :include_extra_info, Types::Params::Bool end request_body do attribute? :name, Types::Params::String attribute? :age, Types::Params::Integer end path_params { attribute :id, Types::Params::Integer } end end ``` We can now use this information to generate swagger documentation, available at the `swagger` action on this controller. More than that, we can use this information *inside* our controller methods: ```ruby def update @person = Person.find(parsed_path.id) @person.update!(parsed_body.to_h) end ``` No need for `params.require` or anything like that. You define the type of parameters you accept, and we reject anything that doesn't fit. ### Typed Responses Want to go further and type your responses too? Use SoberSwag output objects, a serializer library heavily inspired by [Blueprinter](https://github.com/procore/blueprinter) ```ruby PersonOutputObject = SoberSwag::OutputObject.define do field :id, primitive(:Integer) field :name, primitive(:String).optional field :is_registered, primitive(:Bool) do |person| person.registered? end end ``` Now, in your `define` block, you can tell us that this is the *type* of your response: ```ruby class PeopleController < ApplicationController include SoberSwag::Controller define :patch, :update, '/people/{id}' do request_body do attribute? :name, Types::Params::String attribute? :age, Types::Params::Integer end path_params { attribute :id, Types::Params::Integer } response(:ok, 'the updated person', PersonOutputObject) end def update person = Person.find(parsed_path.id) if person.update(parsed_body.to_h) respond!(:ok, person) else render json: person.errors end end end ``` Support for easily typing "render the activerecord errors for me please" is (unfortunately) under development. ### SoberSwag Input Objects Input parameters (including path, query, and request body) are typed using [dry-struct](https://dry-rb.org/gems/dry-struct/1.0/). You don't have to do them inline. You can define them in another file, like so: ```ruby User = SoberSwag.input_object do attribute :name, SoberSwag::Types::String # use ? if attributes are not required attribute? :favorite_movie, SoberSwag::Types::String # use .optional if attributes may be null attribute :age, SoberSwag::Types::Params::Integer.optional end ``` Then, in your controller, just do: ```ruby class PeopleController < ApplicationController include SoberSwag::Controller define :path, :update, '/people/{id}' do request_body(User) path_params { attribute :id, Types::Params::Integer } response(:ok, 'the updated person', PersonOutputObject) end def update # same as above! end end ``` Under the hood, this literally just generates a subclass of `Dry::Struct`. We use the DSL-like method just to make working with Rails' reloading less annoying. #### Adding additional documentation You can use the `.meta` attribute on a type to add additional documentation. Some keys are considered "well-known" and will be present on the swagger output. For example: ```ruby User = SoberSwag.input_object do attribute? :name, SoberSwag::Types::String.meta(description: <<~MARKDOWN, deprecated: true) The given name of the students, with strings encoded as escaped-ASCII. This is used by an internal Cobol microservice from 1968. Please use unicode_name instead unless you are that microservice. MARKDOWN attribute? :unicode_name, SoberSwag::Types::String end ``` This will output the swagger you expect, with a description and a deprecated flag. #### Adding Default Values Sometimes it makes sense to specify a default value. Don't worry, we've got you covered: ```ruby QueryInput = SoberSwag.input_object do attribute :allow_first, SoberSwag::Types::Params::Bool.default(false) # smartly alters type-definition to establish that passing this is not required. end ``` ## Special Thanks This gem is a mishmash of ideas from various sources. The biggest thanks is owed to the [dry-rb](https://github.com/dry-rb) project, upon which the typing of SoberSwag is based. On an API design level, much is owed to [blueprinter](https://github.com/procore/blueprinter) for the serializers. The idea of a strongly-typed API came from the Haskell framework [servant](https://www.servant.dev/). Generating the swagger documenation happens via the use of a catamorphism, which I believe I first really understood thanks to [this medium article by Jared Tobin](https://medium.com/@jaredtobin/practical-recursion-schemes-c10648ec1c29).