Skip to content

pair-finance/open_rails

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenAPI implementation for Ruby On Rails

Usage

Gemfile

gem 'pair_kit_open_rails'

Plugins

class ApplicationController < ActionController::Base
  include PairKit::OpenRails::ControllerPlugin
  include PairKit::ActiveScope::Controller
  
  before_action :authenticate_user!    # use classical Rails Device
  before_action :grant_security_scope!
  
  def grant_security_scope!
    if @current_user.is_active
      @security_scope = ["user:#{@current_user.id}", @current_user.is_admin ? 'admin' : 'user']
    end
  end
end
class ApplicationRecord < ActiveRecord
  include PairKit::ActiveJson::Record
  include PairKit::ActiveScope::Record
end

Example

Define Model

class User < ApplicationRecord
  belongs_to :company
  has_many :tasks 
  
  def full_name
    "#{first_name} #{last_name}"  
  end
  
  security_scope('user:{id}') { |id| where(id: id) } # filter by user
  security_scope(:admin) # full access
  
  schema do
    # own properties
    prop (:id)        .int   .ro                         .r(:admin)
    prop!(:first_name).str                               .rw(:admin, :user)
    prop!(:last_name) .str                               .rw(:admin, :user)
    prop!(:email)     .email                             .rw(:admin).r(:user)
    prop (:full_name) .str   .ro(:first_name, :last_name).r(:admin, :user)
    prop (:is_active) .bool  .defaut(true)               .rw(:admin)
    prop (:is_admin)  .bool  .defaut(false)              .rw(:admin)
    prop!(:company_id).int                               .rw(:admin)
    
    prop!(:password)  .passwd.wo                         .w(:user)
    prop!(:confirm)   .passwd.wo                         .w(:user)
    
    # relations 
    prop(:company)    .object { ref('company') }.ro      .r(:admin)
    prop(:tasks)      .arr { items.ref('task') }.ro      .r(:admin, :user)
  end
end
class Company < ApplicationRecord
  has_many :users
  
  security_scope('user:{id}') { |id| where(id: User.find(id).company_id) } # filter by user
  security_scope(:admin) # full access
  
  schema do
    # own properties
    prop (:id)  .pk .r(:admin)
    prop!(:name).str.rw(:admin).r(:user)
  end
end
class Task < ApplicationRecord
  belongs_to :user
  
  security_scope('user:{id}') { |id| where(user_id: id) } # filter by user
  security_scope(:admin) # full access
  
  schema do
    # own properties
    prop (:id)      .pk                      .r(:admin, :user)
    prop!(:name)    .str                     .rw(:admin, :user)
    prop!(:due_date).date                    .rw(:admin, :user)
    prop(:user)     .object { ref(:user) }.ro.r(:admin)
  end
end
class Order < ApplicationRecord
  belongs_to :company
  
  security_scope('user:{id}') { |id| where(company_id: User.find(id).company_id) } # filter by user
  security_scope(:admin) # full access
  
  STATES = %i[new processing done]
  schema do
    # own properties
    prop (:id)          .pk                         .r(:admin, :user)
    prop!(:date)        .date                    .ro.r(:admin, :user)
    prop!(:amount_cents).int { gt(0) }              .rw(:admin, :user)
    prop!(:status)      .enum(*STATES)              .rw(:admin, :user)
    prop!(:company_id)  .fk                         .rw(:admin).r(:user)
    prop (:company)     .object { ref(:company) }.ro.r(:admin)
  end
end

Define Controller

class UsersController < ApplicationController
  before_openapi_action{ @users ||= security_scoped(User) }
  before_openapi_action(except: %i[create index]) { |params| @user ||= @users.find(params[:id]) }
  
  openapi_tags :users 
  
  openapi_action :create do
    summary 'Create User'

    request.content.content.dynamic_ref('#/components/schemas/models/users:create;security_scope={security_scope}')
    
    response(:created).content.ref('#/components/schemas/models/users:read;security_scope={security_scope}')
    response(:bad_request).content.ref('#/components/schemas/ErrorModel')

    perform { |input| @users.create(input) }
  end
  
  openapi_action :update do
    summary 'Update User'
    
    security_scope :admin, :user
    
    param(:id).in_path.int
    request.content.content.dynamic_ref('#/components/schemas/models/users:update;security_scope={security_scope}')
    
    response(:ok).content.dynamic_ref('#/components/schemas/models/users:read;security_scope={security_scope}')
    response(:bad_request).content.ref('#/components/schemas/ErrorModel')
    response(:not_found)
    
    perform { |_, input| @user.update(input) }
  end
  
  openapi_action :delete do
    summary 'Delete user'

    security_scope :admin

    param(:id).in_path.int
    
    response(:no_content)
    response(:forbidden).content.object.prop(:message).str

    perform do 
      if @user && @user.company.users.count > 1
        throw :forbidden, { message: "Can't delete last user in the company" }
      end
      
      @user&.destroy  
    end
  end
  
  openapi_action :show do
    summary 'Get User'

    security_scope :admin, :user
    
    param(:id).in_path.int

    response(:ok).content.ref('#/components/schemas/models/users:create;scope={security_scope}')
    response(:not_found)

    perform { @user }
  end

  openapi_action :index do
    summary 'Index Users'

    security_scope :admin

    param(:jql).in_query.schema.ref('#/components/schemas/models/users:jql;scope={security_scope}')
    
    response(:ok).content.arr.items.ref('#/components/schemas/models/users:show;scope={security_scope}')

    perform { |params| @users.json_path(params[:jql]) }
  end
end

Use Console Client on Your Local Machine

  client = PairKit::OpenApi::Client.new('https://pairfincne.com/openapi/v.1.1', 
                                        cert: '~/.ssh/pair_api_cert.pem')

  client.users[23].update(email: 'test@test.com')

  query = <<-JQL
    $[?(@amount_cents > 10000, @status = "new"), ^(@amount_cents-, @debtor.last_name+), 100:200]
      {id, amount_cents, debtor{last_name, email}}
  JQL

  client.companies[1238].orders[jql: query].lazy.each do |cf|
    puts "id=#{cd.id} email=#{cf.debtor.email}"  
  end
class OrderController < Create
  before_openapi_action { @orders ||= security_scoped(Order) }
  before_openapi_action(except: %i[create index]) { |params| @order ||= @orders.find(params[:id]) }

  
  openapi_action :create do
    summary 'Create Order'
    
    response(:created).content.ref('#/components/schemas/models/users:read;security_scope={security_scope}')
    response(:bad_request).content.ref('#/components/schemas/ErrorModel')

    request.content.ref('#/components/schemas/models/orders:create;security_scope={security_scope}')
    security_scope :admin, :user
    
    perform { |input| @orders.create(input) } 
  end
end

Other Features

  • ACL on operations and fields level
  • Cashing (almost for free HTTP spec)
  • Support for many media types (like csv, exel) on the library level (without a line custom code)
  • Automatic API client generation (JavaScript, TypoScript, Python) for internal usage and customers
  • Progressive migration, i.e. classical Rails app can bin converted into open API action by action.

TODO

  • Floating input/output schema dependent on params (we need this for conditions like "max number of days depedent on the company")
  • What if I create some sub-graph of data (like order->items), how to orchanize dynamic validation in the context? For instance if I create an order how do I validate that should not be more then 5 open orders per company? Obviously this business logic validation goes to the performer.

Releases

No releases published

Packages

No packages published

Languages