{"id":15903681,"url":"https://github.com/dux/joshua","last_synced_at":"2025-03-20T20:30:54.543Z","repository":{"id":56879249,"uuid":"238239455","full_name":"dux/joshua","owner":"dux","description":"Framework agnostic REST / JSON-RPC API implementation (Ruby lang)","archived":false,"fork":false,"pushed_at":"2023-10-26T00:21:20.000Z","size":458,"stargazers_count":2,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-04-26T13:01:12.069Z","etag":null,"topics":["api","json-rpc","rack","rest","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dux.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-04T15:32:05.000Z","updated_at":"2022-11-17T18:36:35.000Z","dependencies_parsed_at":"2024-10-06T12:03:55.626Z","dependency_job_id":"a741a490-fcd5-4b74-b46d-12df0a03ef05","html_url":"https://github.com/dux/joshua","commit_stats":{"total_commits":53,"total_committers":1,"mean_commits":53.0,"dds":0.0,"last_synced_commit":"203975220fe72113ee27377a649499d1c4965a87"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dux%2Fjoshua","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dux%2Fjoshua/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dux%2Fjoshua/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dux%2Fjoshua/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dux","download_url":"https://codeload.github.com/dux/joshua/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244685439,"owners_count":20493269,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","json-rpc","rack","rest","ruby"],"created_at":"2024-10-06T12:03:41.694Z","updated_at":"2025-03-20T20:30:54.119Z","avatar_url":"https://github.com/dux.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://i.imgur.com/HWoUz5k.png\" align=\"right\" /\u003e\n\n# Joshua \u003csmall\u003e\u0026mdash; Fast Ruby API\u003c/small\u003e\n\nJoshua is opinionated [API](https://learn.g2.com/api) implementation for [Ruby](https://www.ruby-lang.org/en/) based clients, featuring automount for [rack](https://github.com/rack/rack) based clients.\n\n### Features\n\n* Can work in REST or JSON RPC mode.\n* Automatic routing + can be mounted as a Rack app, without framework, for unmatched speed and low memory usage\n* Automatic documentation builder \u0026 [Postman](https://www.postman.com/) / [Insomnia](https://insomnia.rest/) import link\n* Nearly nothing to learn, pure Ruby clases\n* Consistent and predictable request and response flow\n* Errors and messages are [localized](https://github.com/dux/joshua/blob/master/lib/joshua/params/types_errors.rb)\n\n\u003cbr /\u003e\n\n### Installation\n\nto install\n\n`gem install joshua`\n\nor in Gemfile\n\n`gem 'joshua'`\n\nor in Gemfile from GitHub\n\n`gem 'joshua', git: 'git@github.com:dux/joshua.git'`\n\nand to use\n\n`require 'joshua'`\n\n### Components\n\n* Request Flow\n  * [REST / JSON RPC](#rest)\n  * [auto mount](#auto_mount)\n  * [manual mount](#manual_mount)\n  * [automatic routing](#routing)\n* API Methods\n  * [collections or members](#before)\n  * [helper methods](#helper_methods)\n  * [description and detail](#desc_and_detail)\n  * [annotations + custom annotations](#annotations)\n  * [params + custom params](#params)\n    * [models](#models)\n* [Response](#response)\n  * [model exporters](#exporters)\n  * [errors + custom errors](#errors)\n  * success\n    * meta info\n    * [message](#message)\n* Class methods\n  * [rescue from](#rescue_from)\n  * [before and after](#before)\n  * [extending and including](#extending)\n* [Doc builder](#doc_builder)\n\n\n### Speed\n\nJoshua **directly** maps requests to method calls, without routing and it also can work mounted directly on the rack interface, as demonstrated [here](https://github.com/dux/joshua/blob/master/demos/simple/config.ru).\n\nBy using plain ruby classes, direct mapping without routing and provideing direct `rack` access if needed, it is hard to beat Egoist in pure speed.\n\n### Look and feel\n\n* `member (/foo/123/bar)` and `collection (/foo/bar)` exist in separate namespace. You can have `member` and `collection` `update` methods if you need to.\n* `rescue_from`, `before` and `after` filters are supported.\n* you can inherit methods from parent class just as in plain ruby. Define generic `show`, `create`, `update` and `delete` methods and inherit them in parent classes.\n* many more stuff\n\n```ruby\nclass ModelApi \u003c Joshua\n  rescue_from Policy::Error do |error|\n    error 403, 'Policy error: %s' % error.message\n  end\n\n  before do\n    @current_user = User.find_by token: @api.bearer\n  end\n\n  member do\n    def update\n    end\n  end\n\n  after do\n    response[:ip] = @api.request.ip\n  end\nend\n\nclass UsersApi \u003c ModelApi\n  collection do\n    # you can define methods as ruby methods\n    desc   'Login test'\n    detail 'user + pass = foo + bar'\n    params do\n      user\n      pass\n    end\n    def login\n      if params.user == 'foo' \u0026\u0026 params.pass == 'bar'\n        message 'Login ok'\n\n        'token-abcdefg'\n      else\n        error 'Bad user name or pass'\n      end\n    end\n\n    # or wrap them in define block for better visual semantics\n    define :login do\n      desc   'Login test'\n      detail 'user + pass = foo + bar'\n      params do\n        user\n        pass\n      end\n      proc do # or lambda or anything that responds to call\n        if params.user == 'foo' \u0026\u0026 params.pass == 'bar'\n          message 'Login ok'\n\n          'token-abcdefg'\n        else\n          error 'Bad user name or pass'\n        end\n      end\n    end\n  end\n\n  member do\n    def update\n    end\n\n    define :delete do\n      lambda {}\n    end\n  end\n\n  def helper_method\n  end\nend\n```\n\n### Annotated example\n\nFeaturing **nearly all** you have to know to start building your APIs using Joshua.\n\n```ruby\n# in ApplicationApi we will define rules that will reflect all other API classes\nclass ApplicationAPI \u003c Joshua\n  # inside ot the methods you can say `error :foo` and text error will raise\n  rescue_from :foo, 'Baz is angry'\n\n  # capture Policy::Error and add custom formating\n  rescue_from Policy::Error do |error|\n    error 403, 'Policy error: %s' % error.message\n  end\n\n  # define method annotation that will be run before the method executes\n  annotation :anonymous do\n    @anonymous_allowed = true\n  end\n\n  # define custom paramter called label\n  # that will allow only characters, with max length of 15\n  params :label do |value, opts|\n    error 'Label is not in the right format' unless value =~ /^\\w{1,15}$/\n  end\n\n  # before block wil be executed before any method call\n  before do\n    # if token provided load user, raise error otherwise\n    if @api.bearer\n      @current_user = User.find_by token: @api.bearer\n\n      # raise unless user found\n      error 'Invalid API token' unless @current_user\n    end\n\n    # raise error unless @user defined and we dot allow anonymous access\n    if !@user \u0026\u0026 !@anonymous_allowed\n      error 'Anonymous access not allowed,please register'\n    end\n\n    # we will use this time to calcualte method execution speed\n    @_time = Time.now\n  end\n\n  # after block will be run after api method executes\n  after do\n    # add meta tag request.ip if request object is available\n    response[:ip] = @api.request ? @api.request.ip : '1.2.3.4'\n\n    # add meta tag speed in ms\n    response[:speed] = ((Time.now - @_time)*1000).round(3)\n  end\n\n  # `user` method will be available in member and collection methods\n  def user\n    # Raise and return error if user requested but not found\n    @current_user || error('User not loaded')\n  end\nend\n\n\n# we will create generic ModelAPI, that all models will inherit from\nclass ModelAPI \u003c ApplicationAPI\n  # eexecute before all methods that inherit from ModelAPI\n  before do\n    # load generic object based on current class name\n    # UsersApi -\u003e User\n    base = self\n      .class\n      .to_s\n      .sub(/Api$/, '')\n      .singularize\n      .constantize\n\n    # try to load the object\n    if @api.id\n      @model = base.find @api.id\n      error 'Object %s[%s] is not found' % [base, @api.id] unless @model\n    else\n      @model = base.new\n    end\n\n    # raise error unless object not found\n    error 404, 'Object %s[%s] is not found' % [base, @api.id] unless @model\n  end\n\n  # execute after method exection, only in member methods\n  after do\n    # add object path to response\n    response[:path] = @model.path\n  end\nend\n\n\n# example API class for User model\nclass UsersApi \u003c ModelAPI\n  # document this class in various documentations\n  documented\n\n  # define methods for methods that do not need id\n  collection do\n    # describe the method\n    desc 'Signup via email to app'\n    # define email param, with type of email, required\n    params do\n      email :email\n    end\n    # define \"/api/users/signup\" method\n    def signup\n      # deliver magic login link\n      Mailer.email_login_magic_link(params.email).deliver\n\n      # add response message\n      message 'Email with login link sent to %s' % params.email\n    end\n\n    # params can be defined as a block as well\n    params do\n      # method name in a block is paramter name, and it is required\n      # String is defult type, you can skip writeing it\n      user String\n      # if you add question mark, it is not required\n      pass? :string\n    end\n    # /api/users/login\n    unsafe\n    def login\n      if params.user == 'foo' \u0026\u0026 params.pass == 'bar'\n        User.first.token\n      else\n        error 'Wrong user or pass'\n      end\n    end\n  end\n\n  member do\n    before do\n      @user = @model\n\n      # unless user is admin\n      unless user.can.admin?\n        # do not allow him to access member methods in UsersApi class\n        if @user.id != user.id\n          error('This is not you! Hack attempt logged :)')\n        end\n      end\n    end\n\n    # allow access via GET\n    allow :get\n    # /api/users/:id/show\n    def show\n      # export object hash\n      @user.api_export\n    end\n\n    # /api/users/:id/show\n    def delete\n      @user.destroy\n      message 'You deleted yourself'\n    end\n\n    # you can use define to create an api method, to have all nested under readable block\n    # just be sure that you return a proc or labmda as a last argument\n    # /api/users/:id/re_tokenize\n    define :re_tokenize do\n      desc 'Generate new user access token'\n      proc do\n        @user.update token: Crypt.random(40)\n        messsage 'New token generated'\n      end\n    end\n  end\nend\n\n\n# Example api call with response\nUserApi.render :login, params: { user: 'foo', pass: 'bar' }\nUserApi.render.login user: 'foo', pass: 'bar' }\n# {\n#   success: true,\n#   message: 'login ok',\n#   meta: { ip: '127.0.0.1' }\n# }\n\nUserApi.render :login, params: { user: 'aaa', pass: 'bbb' }\n# {\n#   success: false,\n#   error: {\n#     messages: ['Wrong user or pass']\n#   },\n#   meta: { ip: '127.0.0.1' }\n# }\n```\n\n\u003cbr /\u003e\n\n## Main features in detail\n\n\u003ca name=\"rest\"\u003e\u003c/a\u003e\n### Can work in REST or JSON RPC mode\n\nBy default API works on POST for all methods and raises error for any other reqest type. You can modify the behaviour by enabling specific methods using for example `allow :get` to allow `HTTP GET`, shortcut `gettable` or `force :get` to only allow `HTTP GET`.\n\n#### Example requests\n\n```bash\n# this POST request will in production by default\ncurl -d 'foo=bar' http://localhost:3000/api/orgs/1/show\n\n# or as JSON RPC style POST\ncurl -d '{\"id\":\"rand\",\"action\":[\"org\",\"1\",\"show\"],\"params\":{\"foo\":\"bar\"}}' http://localhost:3000/api\n\n# this will work only in development (GET request)\ncurl http://localhost:3000/api/orgs/1/show?foo=bar\n```\n\nResponse is consistent because it is generated from [Joshua::Response](https://github.com/dux/joshua/blob/master/lib/joshua/response.rb) class\nbut you can respond with anything you like\n\n```ruby\n  # respond with csv data\n  # /api/user/1/send_csv\n  def send_csv\n    response :csv do\n      @user.generate_csv_data\n    end\n  end\n  # Content-type: application/csv\n  # name, email, ...\n\n  # response with CSV in response data block\n  # /api/user/1/send_csv\n  def send_csv\n    @user.generate_csv_data\n  end\n  # Content-type: application/json\n  # {\n  #   success: true,\n  #   data: 'csv data...'\n  # }\n```\n\n\u003chr /\u003e\n\n\u003ca name=\"routing\"\u003e\u003c/a\u003e\n### Automatic routing\n\nRequests are directly maped to ruby methods\n\nRoutes can have max 3 elements.\n\n* **2 elements, \"collection\" routes without rosource indentifier**\n  \u003cbr\u003e\n  class / collection method\n* ***3 elements***\n  \u003cbr\u003e\n  class / resource-id / member method\n\nExample will say it all\n\n```ruby\nclass UsersApi\n  collection do\n    # /api/users/login\n    def login\n      'login'\n    end\n  end\n\n  member do\n    # /api/users/:id/update\n    def update\n      'update'\n    end\n  end\nend\n\nmodule Parent\n  class Child\n    member do\n      # Note that you separate modules/classes with a dot.\n      # /api/parent.child/:id/nested\n      def nested\n      end\n    end\n  end\nend\n```\n\nIt is possible to have custom routes as `/api/:company/:class/:id/:method` etc but you have to configure that manualy. This is what you get \"out of the box\" by `auto_mount`\n\nThis is **ALL** you have to know about routing.\n\n\u003chr /\u003e\n\n\u003ca name=\"doc_builder\"\u003e\u003c/a\u003e\n### Automatic documentation builder\n\nBeautiful documentation is automaticly build for you, with ready libraries for all popular languages.\n\nTo enable class documenttion add `documented`\n\n```ruby\nclass UserApi \u003c Joshua\n  documented\n  # ...\nend\n```\n\nAssuming that `Joshua` mount point is `/api`\n\n* You will find interactive HTML documentation on `/api`\n* RAW JSON is available on `/api/_/raw`\n* [Postman](https://www.postman.com/) import URL is available on `/api/_/postman`\n\n##### Example screenshot\n\n![Screenshot](https://i.imgur.com/i3bgVHG.png)\n\n\u003chr /\u003e\n\n### Consistent and predictable request and response flow\n\nRouting is automatic and response is generated by [Joshua::Response](https://github.com/dux/joshua/blob/master/lib/joshua/response.rb) class.\n\n```ruby\n# successuful request\n{\n  success: true,\n  id: 'unique-response-id',\n  data: 'csv data...'\n  message: 'Object updated'\n  meta: {\n    foo: :bar\n  }\n}\n\n# request with errors - form submit example\n{\n  success: false,\n  errors: {\n    messages: ['Foo error', 'Bar error'],\n    details: {\n      foo: 'Foo error',\n      bar: 'Bar error'\n    }\n  }\n}\n```\n\n\n\u003cbr /\u003e\n\n## Class methods\n\nMethods avaiable on class level.\n\n\u003ca name=\"rescue_from\"\u003e\u003c/a\u003e\n### Rescue from\n\nSimilar to Rails `rescue_from`. You can call manualy with `error :foo` or `error 404`, capture named errors and format response as you fit.\n\n```ruby\nclass UsersApi \u003c Joshua\n  rescue_from :foo, 'Baz is angry'\n\n  member do\n    # in method\n    def foo\n      error :foo\n    end\n  end\n\n  # capture Policy::Error and add custom formating\n  rescue_from Policy::Error do |error|\n    error 403, 'Policy error: %s' % error.message\n  end\n\n  collection do\n    # in method\n    def foo\n      @user.can.admin! # triggers Policy::Error, gets captured\n    end\n  end\nend\n```\n\n\u003ca name=\"annotations\"\u003e\u003c/a\u003e\n### Annotations\n\nAnnotations enable us to add API method annotations\n\n#### Example: guest access\n\nCase: If we add `let_guests_in!` annotation we enable guests to use the method.\n\n```ruby\n# define method annotation that will be run before the method executes\nannotation :let_guests_in! do\n  @guets_allowed = true\nend\n\nbefore do\n  # before filter picks up annotation and can be used in logic\n  error 'Guest access not allowed' unless @user || @guets_allowed\nend\n\ncollection do\n  let_guests_in! # annotation used\n  def login\n    error 'This will never trigger' unless @user || @guets_allowed\n    # ...\n  end\nend\n```\n\n#### Example: working hcaptcha.com / recaptha\n\nCase: If we add `hcaptcha` annotation we enusre that `https://hcaptcha.com` check is passed\n\n```ruby\nannotation :hcaptcha! do\n  captcha = params['h-captcha-response'] || error('Captcha not selected')\n  data    = JSON.parse `curl -d \"response=#{captcha}\u0026secret=#{Lux.secrets.hcaptcha.secret}\" -X POST https://hcaptcha.com/siteverify`\n\n  unless data['success']\n    error 'HCaptcha error: %s' % data['error-codes'].join(', ')\n  end\nend\n\ncollection do\n  define :lost_password do\n    desc 'Lost password email (hcaptcha required)'\n    hcaptcha!\n    params do\n      email :email\n    end\n    proc do\n      Mailer.lost_pass params.email\n      'Mail sent'\n    end\n  end\nend\n\n```\n\n\u003ca name=\"params\"\u003e\u003c/a\u003e\n### Params\n\n* you can define params directly on the params metod or you can pass as a block\n* every param can have `optional: true` or end name with `?`\n\n  ```ruby\n  # inline\n  params :full_name, min: 2, max: 40\n\n  # inline optional\n  params.full_name?                        # default String, required: false\n  params.full_name String, required: false # same\n  params.full_name String, optional: true # same\n\n  # as a block\n  params do\n    user_email? :email                  # type: :email, required: false\n    user_email  :email, req: true       # type: :email, required: true\n    user_email  :email, required: true  # type: :email, required: true\n  end\n  ```\n\n* every param can have `default:` value that will be applies if value is `blank?`\n* min and max are available for Integer, Float\n\n  ```ruby\n  params do\n    price Integer, min: 20, max: 100000, default: 1000\n  end\n  ```\n\n* boolean types can be defined in 3 ways\n\n  ```ruby\n  params do\n    is_active :boolean # { type: :boolean, default: false }\n    is_active false    # { type: :boolean, default: false }\n    is_active true     # { type: :boolean, default: true }\n  end\n  ```\n\n* array types are supported\n\n  ```ruby\n  params do\n    labels Array[:label] # Collection\n    labels Set[:label]   # In Set duplicates are discarded\n\n    # if data is provided in a string and not in a Array value\n    # you can define a delimiter that will split String to Array\n    labels Array[:label], delimiter: /\\s*,\\s*/\n  end\n  ```\n\n* many supported types and you can define your own types\n  * native - `:integer`, `:float`, `:date`, `:datetime`, `:boolean`, `:hash`\n  * custom - `:email`, `:url`, `:oib`, `:point` (geo point)\n  * you can as well \u003ca href=\"#params\"\u003edefine your custom type\u003c/a\u003e\n\n#### Define custom params type\n\nYou can define custom param type\n\n* first argument is param type\n* second argument is param options\n* you must return value, value coarse is possible (as memonstrated below)\n\n```ruby\n# define custom paramter called label\n# that will allow only characters, with max length of 15\nparams :locale do |value, opts|\n  # allow 'en' or 'en-gb'\n  error 'Length should be 2 or max 5 chars' unless [2, 5].include?(value.\n  error 'Local is not in the right format' unless value =~ /^[\\w\\-]+$/\n  value.downcase\nend\n\nmember do\n  params do\n    projet_locale :locale\n  end\n  def project\n    # ...\n  end\nend\n```\n\n\u003ca name=\"before\"\u003e\u003c/a\u003e\n### Before and after \u0026 members and collections\n\n* `before` and `after` filters\n  * if defined in root,  fill be triggerd on every API method call.\n  * if nested under `member` and `collection` will be run only in `member` and `collection` api methods.\n* `collection` api methods\n  * can be written as `collection do ...` or `collections do ...`\n  * will run methods when resource ID is NOT provided\n    * example route `/api/users/login`\n* `member` api methods\n  * can be written as `member do ...` or `members do ...`\n  * will run methods when resource ID is provided\n    * example route `/api/users/123/show` or `/api/users/abc-def/show`\n    * accessible via `@api.id (type: String)`\n\nExample\n\n```ruby\nclass TestApi \u003c Joshua\n  # before block wil be executed before any method call\n  before do\n    @num = 1\n  end\n\n  # after will be run after the method executes\n  after do\n    # ...\n  end\n\n  collection do\n    # /api/user/foo\n    def foo\n      @num + foo # 1 + 3 = 4\n    end\n  end\n\n  member do\n    # if defined in `member` of `collection`\n    # it will be called ONLY in respected groups.\n    before do\n      @num += 2\n    end\n\n    # execute after member methods\n    after do\n      # ...\n    end\n\n    # /api/user/:id/foo\n    def foo\n      @num + foo # (1 + 2) + 3 = 6\n    end\n  end\n\n  # this will not be in collision with member or collection methods\n  # any method that is not inside member or collection is a member method\n  def foo\n    3\n  end\nend\n```\n\n### after_auto_mount\n\nIf you want to modify api request after mount. first parameter is class+method path and second is all options hash.\n\n```ruby\n# /api/cisco/contracts/list\n# covert to\n# /api/contracts/list?org_id=123\n\nafter_auto_mount do |nav, opts|\n  if org = Org.find_by code: nav.first\n    nav.shift\n    opts[:params][:org_id] = @org.id\n  end\nend\n```\n\n### unsafe\n\nMethods marked as unsafe will set option `@api.opts.unsafe == true`\n\nYou can use that information not to check for bearer auth token in `before` filter.\n\n\u003ca name=\"models\"\u003e\u003c/a\u003e\n### Models\n\nAPI models can be defined and paramterers can be checked against the models\n\n```ruby\nclass ApplicaitonApi\n  model :company do\n    id      Integer\n    name    String\n    address :address\n  end\n\n  model User do\n    id       Integer\n    name     String\n    email    :email\n    is_admin :boolean\n\n    # If proc is defined and returned, filtering will be applied\n    #   before the data is forwarded to api method\n    # In this case raise error is :is_admin attribute is defined but user\n    #   is not allowed to change it\n    proc do |data|\n      if !data[:is_admin].nil? \u0026\u0026 !user.can.admin?\n        error 'You are not allowed change the value of :is_admin attribute'\n      end\n    end\n  end\nend\n\nclass UserApi\n  members do\n    desc 'Update user options'\n    params do\n      user model: User\n    end\n    def update\n      # ...\n    end\n  end\nend\n```\n\n## API methods - inline methods\n\nJoshua specific methods you can call inside API methods (ones in `member` or `collection` blocks)\n\n### error\n\nIf you want to manualy trigger errors\n\n```ruby\nrescue_from :foo do |error|\n  error 403, 'Policy error'\nend\n\ndef foo\n  # trigger named erorr\n  error :foo       # { success: false, code: 403, error: { messages: ['Policy error'] }}\n\n  # default response status is 400\n  error 'foo bar'      # { success: false, code: 400, error: { messages: ['foo bar'] }}\n\n  # you can define response status\n  error 404, 'foo' # { success: false, code: 404, error: { messages: ['foo'] }}\nend\n\n```\n\u003ca name=\"response\"\u003e\u003c/a\u003e\n### response\n\nResponse object is responsible for response render\n\n```ruby\n  # respond with csv data\n  # /api/user/1/send_csv\n  def send_csv\n    response :csv do\n      @user.generate_csv_data\n    end\n  end\n  # Content-type: application/csv\n  # name, email, ...\n\n  # response with CSV in response data block\n  # /api/user/1/send_csv\n  def send_csv\n    # add \"foo\" meta response key with value\n    response[:foo] = :bar\n    # the same\n    response.meta :foo, :bar\n\n    # access rack response header\n    response.header['content-type'] = 'application/foo'\n\n    # force response.status 404\n    error 404, 'Object not found'\n    # defaults to status: 400\n    error 'Object not found'\n\n    # check if response has errors\n    response.error?\n\n    # manual set response data\n    response.data = :foo\n\n    @user.generate_csv_data\n  end\n  # Content-type: application/json\n  # {\n  #   success: true,\n  #   data: 'csv data...'\n  #   message: 'Object updated'\n  #   meta: {\n  #     foo: :bar\n  #   }\n  # }\n```\n\n\u003ca name=\"message\"\u003e\u003c/a\u003e\n### message\n\nMessage method sends message to response.\n\n```ruby\ndef update\n  # add response message\n  message 'Object updated'\n  :foo\nend\n# {\n#   success: true,\n#   message: 'Object updated'\n#   data: 'foo'\n# }\n```\n\n\u003ca name=\"helper_methods\"\u003e\u003c/a\u003e\n### helper methods\n\nHelper methods are all instance methods defined outside `member` or `collection` scopes\n\n\u003ca name=\"errors\"\u003e\u003c/a\u003e\n### response errors\n\nYou are free to use all HTTP error status codes, but we suggest to use only `400` for handled errors and `500` for unhandled errors, and of course, try to provide nice error descriptions.\n\n#### Example\n\n```ruby\nrescue_from :big_load do\n  custom_looger :load_too_big\n  error 'There is to big load on the API, please try again or sign up for priority access'\nend\n\ndef foo\n  # response.status: 400\n  error 'Object not found'\n\n  # response.status 400, error.code: 404\n  error 'Object not found', code: 404\n\n  # response.status 404, error.code: 404\n  error 'Object not found', status: 404, code: 404\n\n  # unhandled, response.status: 500\n  raise 'Some erorr'\n\n  # execute rescued :big_load\n  error :big_load\nend\n```\n\n### @api - instance variable\n\nJoshua is not polluting scope with various instance varaibles. Only `@api` variable is used.\n\nBasicly, this are options passed to `initialize` or `auto_mount` + instance specifics.\n\n```ruby\ndef foo\n  @api.action == :foo # true\nend\n```\n\n* `@api.action`        - original triggered action\n* `@api.bearer`        - Bearer that is passed in or from a `Auth` [header](https://stackoverflow.com/questions/22229996/basic-http-and-bearer-token-authentication)\n* `@api.development`   - `true` or `false`. In development mode\n* `@api.id`            - in `member` methods, this will be resource ID.\n* `@api.opts`          - Options passed to initializer\n* `@api.params`        - Method params hash\n* `@api.rack_response` - original rack response object\n* `@api.request`       - original rack request object\n* `@api.response`      - internal response object, accessible from `response` method\n* `@api.uid`           - if using JSON RPC and id is passed, it will be stored here\n\n\u003ca name=\"extending\"\u003e\u003c/a\u003e\n## Extending, mounting, including\n\nThere is no `mount`, you just include ruby files like you would to with any other ruby class.\n\nThere are 2 ways to create modules ready for inlude\n\n### Plain ruby\n\nDefine a module and include it as you would do with any other ruby class.\n\n```ruby\nmodule ApiModuleClasic\n  def self.included base\n    base.collection do\n      def foo\n        message 'bar'\n      end\n    end\n  end\nend\n\nclass UserApi \u003c Joshua\n  include ApiModuleClasic\nend\n\n# /api/user/foo # { message: 'bar' }\n```\n\n### Calling super methods\n\nIf you want to call `super` to call super method inside api methods, you need to call them with `super!`. You can also pass a super method name as a argument.\n\n```ruby\nclass ParentApi \u003c Joshua\n  collection do\n    def foo\n      123\n    end\n  end\nend\n\nclass ChildApi \u003c ParentApi\n  collection do\n    def foo\n      super! # 122\n      345\n    end\n\n    def bar\n      foo         # 345\n      super! :foo # 123\n    end\n  end\nend\n```\n\n### As a plugin\n\nPlugin inteface has few lines less.\n\n```ruby\nJoshua.plugin :foo_bar do\n  collection do\n    def foo\n      message 'baz'\n    end\n  end\nend\n\nclass UserApi \u003c Joshua\n  plugin :foo_bar\nend\n\n# /api/user/foo # { message: 'baz' }\n```\n\n## Initializing\n\nThere are a three basic ways you can initialize yor app\n\n### 1.using config.ru - withouth framework\n\nThis is the fasted way with best memory usage.\n\nIf you clone this repo and run `puma -p 4000` in root, you can see how local example works.\n\n\n```ruby\nrequire_relative 'joshua'\n\nclass ApplicationApi \u003c Joshua\nend\n\nclass UsersApi \u003c ApplicationApi\n  collection do\n    def login\n      'To do'\n    end\n  end\nend\n\nrun ApplicationApi\n# /users/login -\u003e { success: true, data: 'To do' }\n```\n\n\u003ca name=\"auto_mount\"\u003e\u003c/a\u003e\n### 2. auto mounting\n\n#### Using [Sinantra](http://sinatrarb.com/)\n\n```ruby\n# this will mount api in /api endpoint\npost '/api*' do\n  ApplicationApi.auto_mount mount_on: '/api',\n    request: request,\n    response: response,\n    development: ENV['RACK_ENV'] == 'development'\nend\n```\n\n#### Using [Ruby On Rals](https://rubyonrails.org/)\n\n```ruby\n# config/routes.rb\nmount ApplicationApi =\u003e '/api'\n# or\nmatch '/api/**', to: 'api#mount', via: [:get, :post]\n\n# app/controllers/api_controller.rb\nclass ApiController \u003c ApplicationController         \n  def mount \n    ApplicationApi.auto_mount mount_on: '/api',\n      api_host: self,\n      bearer: user.try(:token),\n      development: Rails.env.development?\n  end\nend\n```\n\n\u003ca name=\"manual_mount\"\u003e\u003c/a\u003e\n### 3. Manual mount\n\nWhen manualy mounting APIs, you need to use specific Joshua endpoint and return the resposnse.\n\n```ruby\npost '/api/users/index' do\n  result = UsersApi.render :index\n  my_format_api_response result\nend\n```\n\n## Testing \u0026 non api usage\n\nNo testing helpers provided (for now)\n\nUse this for easy access (get response `Hash`)\n\n```ruby\n# call user collection method login\nUserApi.render.login(user: 'foo', pass: 'bar')\n\n# call user member method show\nUserApi.render.show(123)\n\n# call user member method foo\nUserApi.render.foo(123, bar: 'baz')\n\n# or wih user token expanded\nUserApi.render :foo, id: 123, bearer: @user.token, params: { bar: 'baz' }\n```\n\n## Demos\n\n* Simple demo, runnable rack app https://github.com/dux/joshua/tree/master/demos/simple\n* Real life AppicationApi, BaseModelApi, UserApiSimple demo https://github.com/dux/joshua/tree/master/demos/inherited-model\n\n## Dependencies\n\n* **rack** - basic request, response lib\n* **json** - better JSON export\n* **http** - for JoshuaClient\n* **dry-inflector** - `classify`, `constantize`, ...\n* **html-tag** - for documention builder\n* **clean-hash** - for params in api methods\n\n## Development\n\nAfter checking out the repo, run bundle install to install dependencies. Then, run rspec to run the tests.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/dux/joshua. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdux%2Fjoshua","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdux%2Fjoshua","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdux%2Fjoshua/lists"}