Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mountapi

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mountapi

  • 0.11.1
  • Rubygems
  • Socket score

Version published
Maintainers
1
Created
Source

Mountapi

Given an OpenAPI 3 spec:

  • expose routes through rack http endpoints
  • type cast request parameters against specs
  • validate request parameters against specs
  • call handler with nice parameter hash
  • format responses
  • handle parameters failure, and route not found errors with details and status codes

For examples see example and test/mountapi/rack_app_test.rb

Important warning

If you found this gem while browsing Rubygems, be aware that this release is still experimental and meant to be use internally only.

We will not provide any support regarding this gem. There are some issues we want to sort out before releasing a real public version.

For instance, we had to fork the json-schema gem to include a fix. We can't reference this fork from github since gemspec doesn't allow this. As a result, we put our fork in vendor directory which is not the cleanest thing to do but the only one working at the moment.

Installation

Add this line to your application's Gemfile:

gem 'mountapi'

And then execute:

$ bundle

Or install it yourself as:

$ gem install mountapi

Usage

Tutorial

Rails
# Var env BUNDLE_LOCAL__XXX is no longer used by bundler
# because we don't have a specified branch
# But we use it manually to set the local path
def private_gem(name, tag: nil, path: nil)
  var_env = ENV["BUNDLE_LOCAL__#{name.upcase}"]

  if var_env.nil? || var_env.empty?
    gem name, tag
  elsif File.directory?(path)
    gem name, path: path
  end
end

source "https://gems.synbioz.com" do
  private_gem "mountapi", tag: "0.6.0", path: "/mountapi"
end

With this helper you can work with the gem locally in the context. You have to specify the variable BUNDLE_LOCAL__MOUNTAPI=/mountapi in ops/dev/app_env and mount a specific volume in the docker-compose of dev:

services:
  app:
  ...
  volumes:
    - app_data:/app
    - mountapi:/mountapi
  ...

volumes:
  ...
  mountapi:
    driver_opts:
      type: none
      device: "/Users/jonathanfrancois/code/synbioz/mountapi" # CHANGE ME
      o: bind
  ...

Add a dedicated initializer:

Rails.application.configure do
  require "mountapi"

  config.before_initialize do
    config.middleware.insert_before Warden::Manager, Mountapi::RackApp
  end

  HANDLERS = "#{Rails.root}/app/mount_api/handlers"
  require "#{HANDLERS}/base_handler.rb"

  unless Rails.env.production?
    Dir.glob("#{HANDLERS}/**/*.rb") { |f| require f }

    config.to_prepare do
      Dir.glob("#{HANDLERS}/**/*.rb") { |f| load f }
    end
  end
end

Mountapi.configure do |config|
  config.open_api_spec = File.read(File.expand_path("../../api.yaml", __dir__))
end

# Enable documentation
# You must add `mount Mountapi::DocRackApp, at: "/apidoc"` in routes.rb
require "mountapi/doc_rack_app"

Add a valid OpenAPI 3 spec file to rails root directory #{Rails.root}/api.yml. And add your handlers in specific directory #{Rails.root}/app/mount_api/handlers/. It is important to use this folder and to use a Handler module in order to allow Zeitwerk to auto-load the classes.

We will usually use a basic handler and include Mountapi::Handler::Behaviour. By the way, this file is explicitly required in the initializer.

module Handlers
  class BaseHandler
    include Mountapi::Handler::Behaviour

  end
end

One can list all routes detected and handled by MountAPI by using the mountapi:routes Rake task.

Ruby app

Given a powerful micro-service that just return the parameters it receive:

#lib/slice_array.rb

# A dumb service example
module SliceArray
  def self.call(array:, start:, length:)
    array.slice(start, length)
  end
end

And a valid OpenAPI 3 spec file to expose the service

openapi: 3.0.0
info:
  version: 1.0.0
  title: Test
paths:
  /array/slicer:
    get:
      operationId: SliceArray
      parameters:
        - name: target
          in: query
          schema:
            type: array
            items:
              type: string
        - name: limit
          in: query
          schema:
            type: integer
          required: true
        - name: offset
          in: query
          required: true
          schema:
            type: integer

      responses:
        "200":
          description: success
        "400":
          description: error in request

In order to serve your endpoint specification through HTTP, you just need two things:

First Create a handler

#/lib/slice_array/mountapi_handler

module SliceArray
  class MountapiHandler
    include Mountapi::Handler::Behaviour # add the behaviour to the class
    operation_id "SliceArray"
    # You must define a call instance method
    # `ok` and `params` are provided by Handler::Behaviour
    def call
      data = SliceArray.call(params)
      ok(data: data)
    end
  end
end

Second, add a rackup file in order to boot your rack endpoint

#config.ru

require "bundler"
Bundler.require

require "slice-array"
require "slice-array/handler/behaviour"
require "mountapi"
require "mountapi/rack_app"

Mountapi.configure do |config|
  config.open_api_spec = ["../open_api_v1.yml", "../open_api_v2.yml"].map { |path| File.read(File.expand_path("../open_api.yml", __FILE__)) }
end

run Mountapi::RackApp.new

If you rackup you'll get your service wired through HTTP !

curl "localhost:9292/1.0.0/array/slicer?target[]=a&target[]=b&target[]=c&limit=1&offset=1"

{"data":["b"]}%
curl localhost:9292/1.0.0/array/slicer

{"errors":"[\"The property '#/' did not contain a required property of 'target' in schema ca7afc0e-f3a0-5416-92fa-3a72ffff7fe2\", \"The property '#/' did not contain a required property of 'limit' in schema ca7afc0e-f3a0-5416-92fa-3a72ffff7fe2\", \"The property '#/' did not contain a required property of 'offset' in schema ca7afc0e-f3a0-5416-92fa-3a72ffff7fe2\"]"}
curl "localhost:9292/1.0.0/array/slicer?limit=foo&offset=1.0"

{"errors":"[{:message=>\"can't cast \\\"foo\\\" as Mountapi::Schema::Integer\"}]"}%

Play with the example in example/

Requests conventions

  • A JSON request body root MUST be an object
  • Default values will be filled in your parameters only for the root level. Default values in nested object are not supported yet (and should not be, because it would add complexity to applications design and API).

Responses schema and validations

If you define responses payload in the YAML, your hanlder response payload must match this definition. If you don't define a response schema, your response will be validated against JSON API schema.

If the response payload is not valid, Mountapi will raise a Mountapi::Error::InvalidResponse

URL reflection

Within a handler the following methods are available :

  • base_url, that returns current request base URL
  • url_for(operation_id, url_params) that return the URL for a given operation and parameters (eg. url_for("ShowRevision", uid: revision.uid))

Handler implementation

Why a handler ?

The handler is responsible for the integration between infrastructure detail (using Mountapi) and the application services (aka use cases).

Because application code does not know anything about the way you deliver the application to the end user. You could deliver the content through Mountapi , HTML, Terminal or a mobile phone endpoint. Your application business remain the same, only the transport protocol and representation would change.

Implementation

A Moutapi handler is really a dumb but valuable piece of code. It has to transform the inbound parameters from Mountapi to a set of parameters required by your use case.

Then will call an application service to fulfill the request, and translate the execution result to Mountapi response.

see lib/mountapi/handler/behaviour

example :

require "gtc/app/show_revision"

module Gtc
  module Infra
    module Mountapi
      class ShowRevision
        include ::Mountapi::Handler::Behaviour

        operation_id "ShowRevision"

        def call
          data = Gtc::App::ShowRevision.call(params.fetch("gtc_uid"))
          data && ok(data.to_h) || not_found
        end
      end
    end
  end
end
Roles handling

If your API is behind an HTTP proxy which provides headers with current user info then you can take advantage of the user_info_adapter to automatically handle authorization before reaching handler call method.

To take advantage of this, you need to configure user_info_adapter. This must be a callable that must return a boolean. If the return value is true then call method will be run otherwise a 403 Forbidden response will be returned.

Mountapi.configure do |config|
  config.user_info_adapter = Mountapi::Adapters::UserInfoAdapter.new("x-user-info", { roles: "roles" })
end

In the previous example, we're using the provided UserInfoAdapter. It takes two arguments, the first one is the name of the HTTP header the adapter will read to gather info about the user. The second argument is a mapping of adapter internal info and its corresponding name in the header value.

UserInfoAdapter is expecting the header value to be a base64 encoded JSON string. This JSON must contain at least a key which value will be an array of the current user roles.

So in our example we're expecting a x-user-info HTTP header which is a JSON string including a key named roles.

If you need to use HTTP headers in an other way then you can write your own adapter.

Now that our adapter is configured, we can specify allowed roles in our handler:

class SomeHandler
  include Mountapi::Handler::Behaviour
  operation_id "operationId"
  allowed_roles "admin"

  def call
   # do something if user has one of allowed roles
  end
end

In this example, we use the allowed_roles method to specify that only user with role admin should be allowed to execute the action. If one of the current user roles is admin then we run call method. If roles aren't matching then we shortcut the execution by returning a 403 response.

Development

After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

To run containerized tests locally, run ops/test/compose run --rm runner rake test.

API Documentation

Mountapi allows you to generate the documentation associated with your APIs. It uses the Redoc service: https://github.com/Redocly/redoc

To do this you need to require the dedicated app rack and mount it in your routes. For example in the context of a rails project:

# config/initializer/mountapi.rb
...
Mountapi.configure do |config|
  config.open_api_spec = File.read(File.expand_path("../../api.yml", __dir__))
end
# You absolutely must require it after the configuration of the Mountapi gem
require "mountapi/doc_rack_app"

and mount the rack app in your router by specifying the namespace (here apidoc):

# config/routes.rb
mount Mountapi::DocRackApp, at: "/apidoc"

You then can access the documentation of all your versions from the main page /apidoc.

Contributing

Bug reports and pull requests are welcome on GitLab at https://git.synbioz.com/synbioz/mountapi.

FAQs

Package last updated on 28 Jan 2022

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc