base on Flexible API builder gem for Rails # `flappi`
## Flexible API builder gem for Rails
Flappi allows Rails APIs to be defined using a simple DSL that avoids repeated and fragmented code and allows the API definition to reflect the request/response structure.
Support is provided for versioning (semantic with the addition of 'flavours') and documentation (using [apiDoc](http://apidocjs.com/)).
Interface documentation is [here](https://sharesight.github.io/flappi/Flappi.html)
## Quickstart with Rails (4-6)
Add to Gemfile:
gem 'flappi', git: '
[email protected]:sharesight/flappi.git'
Bundle install:
bundle install
Create your initialization file, e.g. in **'config/initializers/flappi.rb'**
```ruby
Flappi.configure do |conf|
conf.definition_paths = { 'default' => 'api_definitions' } # Normally under your controller path
end
```
Create a controller and route, e.g in **'app/controllers/adders_controller'**:
```ruby
class AddersController < ApplicationController
def show
Flappi.build_and_respond(self)
end
end
```
and in **'config/routes.rb'**:
resource :adder
Flappi (currently) uses the regular Rails routing and controller framework, so this is much as for an ordinary controller.
(If you try the endpoint [http://localhost:3000/adder](http://localhost:3000/adder) now, you should get an error like: *'Endpoint Adders is not defined to API Builder'*)
Now define the endpoint using the Flappi DSL. In **'app/controllers/api_definitions/adders.rb'**:
```ruby
module ApiDefinitions
module Adders
include Flappi::Definition
def endpoint
title 'Add numbers'
http_method 'GET'
path '/adder'
# We define two query parameters, 'a' is required
param :a, type: Integer, optional: false
param :b, type: Integer
# IRL, this would probably query your ActiveRecord model, reporting engine
# or other artefact to get a returned record - we just add two numbers together
# the result of this is the context for the response
query do |params|
{result: params[:a].to_i + (params[:b].try(:to_i) || 0) }
end
end
# Build a record with the one result field
# Notice how just specifying a name is enough to access the value
def respond
build do
field :result, type: Integer
end
end
end
end
```
Now, if you access: [http://localhost:3000/adder.json?a=4](http://localhost:3000/adder.json?a=4) you should see the result:
{
"result": 4
}
and similarly [http://localhost:3000/adder.json?a=4&b=22](http://localhost:3000/adder.json?a=4&b=22) (etc)
## Adding Fields to APIs
### 1:1 mapping with the model
Map directly to model attributes like app/controllers/api/v3/api_builder_definitions/shared/currency.rb maps code, symbol and id directly to the app/models/currency.rb objects (and associated enumerables).
### Using the Source Field
In order to add new fields to an API the easiest option is to create an attr_reader on the model you're referencing. For instance app/controllers/api/v3/api_builder_definitions/shared/document.rb has the 4 fields:
[:file_name, :file_size, :created_at, :content_type]
These fields can be whatever you want to use for the api and don't tie to anything in the App itself.
The "source" is where we pull in data for these 4:
[:document_file_name, :document_content_type, :document_file_size, :document_created_at]
These four "sources" are defined in app/models/document.rb as attr_reader methods with the same names as the source.
While its not ideal, if the value you need is not stored ready to go in the database, your attr_reader on the model COULD make a call to a service in order to return the value desired but this isn't really a desirable approach as it shouldn't be the concern of the model. Additional investigation is needed into alternative ways of managing this.
### Using Procs and Lambdas
#### full logic (nested + boolean cast):
field(name: :supported, type: BOOLEAN, doc: '…') { |o| !!o.nominal_country&.ss_support? }
#### re-name an attribute (these examples should be equivalent):
field(:financial_year_end, doc: '…') { |p| p.financial_year_end_s }
field(:financial_year_end, source: :financial_year_end_s, doc: '…')
## Advanced
- [Implementing a POST endpoint](docs/file.POST.html)
- [Nesting structures in a response](docs/file.NEST.html)
- [Sharing fields](docs/file.SHARE.html)
- [Versions](docs/file.VERSIONS.html)
## Contributing
See [CONTRIBUTING](./CONTRIBUTING.md).
## Code of Conduct
See [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md).
", Assign "at most 3 tags" to the expected json: {"id":"9172","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"