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: 'git@github.com: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"