├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── bin ├── console └── setup ├── lib ├── problem_details.rb └── problem_details │ ├── document.rb │ └── version.rb ├── problem_details-rails ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib │ ├── problem_details-rails.rb │ └── problem_details │ │ ├── rails.rb │ │ └── rails │ │ ├── config.rb │ │ ├── railtie.rb │ │ ├── render_option_builder.rb │ │ └── version.rb ├── problem_details-rails.gemspec └── spec │ ├── problem_details │ └── rails │ │ ├── config_spec.rb │ │ └── render_option_builder_spec.rb │ └── spec_helper.rb ├── problem_details.gemspec ├── sinatra-problem_details ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib │ ├── sinatra-problem_details.rb │ └── sinatra │ │ ├── problem_details.rb │ │ └── problem_details │ │ └── version.rb ├── sinatra-problem_details.gemspec └── spec │ ├── problem_spec.rb │ └── spec_helper.rb └── spec ├── problem_details └── document_spec.rb └── spec_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby: [ '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', 'jruby', 'truffleruby' ] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby }} 21 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 22 | 23 | - name: Build and test with RSpec 24 | run: bundle exec rspec 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.3 / 2021-09-03 2 | 3 | * Support the `#to_hash` implicit conversion protocol (#8) by @stevenharman 4 | 5 | ## 0.2.2 / 2019-10-30 6 | 7 | * Remove default about:blank type (#5) by @akrawchyk 8 | 9 | ## 0.2.1 / 2018-03-31 10 | 11 | * Clarify supported ruby/gem versions 12 | 13 | ## 0.2.0 / 2018-03-20 14 | 15 | ### Enhancements: 16 | * Introduce a new gem, `sinatra-problem_details`, the Sinatra problem detail extension. 17 | 18 | ## 0.1.0 / 2018-03-26 19 | 20 | * First release 21 | * Initial implementation of `problem_details` 22 | * Rails renderer feature by `problem_details-rails` 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Nobuhiro Nikushi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProblemDetails 2 | 3 | [![CI](https://github.com/nikushi/problem_details/actions/workflows/ci.yml/badge.svg)](https://github.com/nikushi/problem_details/actions/workflows/ci.yml) 4 | [![Gem Version](https://badge.fury.io/rb/problem_details.svg)](https://badge.fury.io/rb/problem_details) 5 | 6 | ProblemDetails is an implementation of [RFC7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807). 7 | 8 | The RFC defines a "problem detail" as a way to inform errors to clients as machine readable form in a HTTP response 9 | to avoid the need to define new error response formats for HTTP APIs. 10 | 11 | This library also works with Rails and Sinatra, by the `problem` renderer that helps to respond with the problem detail form. 12 | 13 | Currently only JSON serialization is supported. 14 | 15 | ## Features 16 | 17 | * Provides the class that implements a Problem Details JSON Object. 18 | * With Rails, automatically adds the renderer to respond with `Content-Type: application/problem+json` which works with `render` in controllers. 19 | 20 | ## Supported Versions 21 | 22 | * Ruby 2.4.x, 2.5.x, 2.6.x 3.0.x 3.1.x 23 | * Rails 4.x, 5.x 24 | * Sinatra >= 1.4 25 | 26 | ## Installation 27 | 28 | Add this line to your application's Gemfile: 29 | 30 | ```ruby 31 | gem 'problem_details' 32 | ``` 33 | 34 | Or if you use with Rails, add below line instead. 35 | 36 | ```ruby 37 | gem 'problem_details-rails' 38 | ``` 39 | 40 | With Sinatra, add below line instead. 41 | 42 | ```ruby 43 | gem 'sinatra-problem_details' 44 | ``` 45 | 46 | And then execute: 47 | 48 | $ bundle 49 | 50 | ## Usage 51 | 52 | ### Build a problem 53 | 54 | ```ruby 55 | require 'problem_details' 56 | 57 | ProblemDetails::Document.new(status: 404).to_json 58 | 59 | # Or status code symbol can be specified as well. 60 | ProblemDetails::Document.new(status: :not_found).to_json 61 | ``` 62 | 63 | will produce: 64 | 65 | ```json 66 | { 67 | "title": "Not Found", 68 | "status": 404 69 | } 70 | ``` 71 | 72 | As above, the value of `type` is implied to be `about:blank` by default if the value is ommited, also the value of `title` is filled automatically from the status code. These are described in [Predefined Problem Types](https://tools.ietf.org/html/rfc7807#section-4.2): 73 | 74 | > The "about:blank" URI, when used as a problem type, indicates that the problem has no additional semantics beyond that of the HTTP status code. 75 | 76 | > When "about:blank" is used, the title SHOULD be the same as the recommended HTTP status phrase for that code (e.g., "Not Found" for 404, and so on) 77 | 78 | > ..."about:blank" URI is the default value for that ["type"] member. Consequently, any problem details object not carrying an explicit "type" member implicitly uses this URI. 79 | 80 | But you may also have the need to add some little hint, e.g. as a custom detail of the problem: 81 | 82 | ```ruby 83 | ProblemDetails::Document.new(status: 503, detail: 'Database not reachable').to_json 84 | ``` 85 | 86 | will produce: 87 | 88 | ```json 89 | { 90 | "title": "Service Unavailable", 91 | "status": 503, 92 | "detail": "Database not reachable" 93 | } 94 | ``` 95 | 96 | You can build a problem with any additional members which are described as [extension members](https://tools.ietf.org/html/rfc7807#section-3.2). 97 | 98 | ```ruby 99 | ProblemDetails::Document.new( 100 | status: :forbidden, 101 | type: 'https://example.com/probs/out-of-credit', 102 | balance: 30, 103 | accounts: ['/account/12345', '/account/67890'], 104 | ).to_json 105 | ``` 106 | 107 | will produce(note that `balance` and `accounts` are extention members): 108 | 109 | ```json 110 | { 111 | "type": "https://example.com/probs/out-of-credit", 112 | "title": "Forbidden", 113 | "status": 403, 114 | "balance": 30, 115 | "accounts": [ 116 | "/account/12345", 117 | "/account/67890" 118 | ] 119 | } 120 | ``` 121 | 122 | ### With Rails 123 | 124 | Once `problem_details-rails` gem is installed into a Rails system, a problem can be rendered with the problem detail form with `Content-Type: application/problem+json`. 125 | 126 | For example, respond with validation error messages: 127 | 128 | ```ruby 129 | # app/controllers/api/posts_controller.rb 130 | class Api::PostsController < ApplicationController 131 | def create 132 | @post = Post.new(params[:post]) 133 | if @post.save 134 | render json: @post 135 | else 136 | render problem: { errors: @post.errors }, status: :unprocessable_entity 137 | end 138 | end 139 | end 140 | ``` 141 | 142 | With `render problem: { ... }`, generated HTTP response will be like: 143 | 144 | ``` 145 | HTTP/1.1 422 Unprocessable Entity 146 | Content-Type: application/problem+json; charset=utf-8 147 | 148 | { 149 | "title": "Unprocessable Entity", 150 | "status": 422, 151 | "errors": { 152 | "body": [ 153 | "can't be blank" 154 | ] 155 | } 156 | } 157 | ``` 158 | 159 | ### With Sinatra 160 | 161 | Install `sinatra-problems_details` into a sinatra app, a problem is be rendered as well with `Content-Type: application/problem+json`. 162 | 163 | #### Classic Application 164 | 165 | ```ruby 166 | require 'sinatra' 167 | require 'sinatra-problem_details' 168 | 169 | get '/' do 170 | status 400 171 | problem foo: 'bar' 172 | end 173 | ``` 174 | 175 | #### Modular Application 176 | 177 | ```ruby 178 | require 'sinatra/base' 179 | require 'sinatra-problem_details' 180 | 181 | class MyApp < Sinatra::Base 182 | register Sinatra::ProblemDetails 183 | 184 | get '/' do 185 | status 400 186 | problem foo: 'bar' 187 | end 188 | end 189 | ``` 190 | 191 | #### Response 192 | 193 | The sinatra apps defined in the previous sections will render: 194 | 195 | ``` 196 | HTTP/1.1 400 Bad Request 197 | Content-Type: application/problem+json 198 | 199 | { 200 | "title": "Bad Request", 201 | "status": 400, 202 | "foo": "bar" 203 | } 204 | ``` 205 | 206 | ## Development 207 | 208 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 209 | 210 | 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](https://rubygems.org). 211 | 212 | ## Contributing 213 | 214 | Bug reports and pull requests are welcome on GitHub at https://github.com/nikushi/problem_details. 215 | 216 | ## License 217 | 218 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 219 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_helper' 4 | require 'rspec/core/rake_task' 5 | 6 | # For multiple gems 7 | # ref https://github.com/bkeepers/dotenv/blob/master/Rakefile 8 | 9 | # === helpers === 10 | def source_version 11 | @source_version ||= File.read(File.expand_path("../VERSION", __FILE__)).strip 12 | end 13 | 14 | # == release ==== 15 | desc "Commits the version" 16 | task :commit_version do 17 | sh <<-SH 18 | gsed -i "s/.*VERSION.*/ VERSION = '#{source_version}'/" lib/problem_details/version.rb 19 | gsed -i "s/.*VERSION.*/ VERSION = '#{source_version}'/" problem_details-rails/lib/problem_details/rails/version.rb 20 | gsed -i "s/.*VERSION.*/ VERSION = '#{source_version}'/" sinatra-problem_details/lib/sinatra/problem_details/version.rb 21 | SH 22 | 23 | sh "git commit --allow-empty -a -m '#{source_version} release'" 24 | end 25 | 26 | namespace 'problem_details' do 27 | Bundler::GemHelper.install_tasks name: 'problem_details' 28 | end 29 | 30 | namespace 'problem_details-rails' do 31 | class ProblemDetailsRailsGemHelper < Bundler::GemHelper 32 | def guard_already_tagged; end # noop 33 | 34 | def tag_version; end # noop 35 | end 36 | 37 | ProblemDetailsRailsGemHelper.install_tasks dir: File.join(__dir__, 'problem_details-rails'), name: 'problem_details-rails' 38 | end 39 | 40 | namespace 'sinatra-problem_details' do 41 | class SinatraProblemDetailsGemHelper < Bundler::GemHelper 42 | def guard_already_tagged; end # noop 43 | 44 | def tag_version; end # noop 45 | end 46 | 47 | SinatraProblemDetailsGemHelper.install_tasks dir: File.join(__dir__, 'sinatra-problem_details'), name: 'sinatra-problem_details' 48 | end 49 | 50 | desc 'build gem' 51 | task build: ['problem_details:build', 'problem_details-rails:build', 'sinatra-problem_details:build'] 52 | 53 | desc 'build and install' 54 | task install: ['problem_details:install', 'problem_details-rails:install', 'sinatra-problem_details:build'] 55 | 56 | desc 'release' 57 | task release: ['problem_details:release', 'problem_details-rails:release', 'sinatra-problem_details:release'] 58 | 59 | RSpec::Core::RakeTask.new(:spec) 60 | task default: 'spec:all' 61 | 62 | namespace :spec do 63 | dirs = %w( 64 | . 65 | problem_details-rails 66 | sinatra-problem_details 67 | ) 68 | 69 | desc "Run all specs" 70 | task :all do 71 | dirs.each do |d| 72 | Dir.chdir(d) do 73 | Bundler.with_clean_env do 74 | sh 'bundle --quiet' 75 | sh 'bundle exec rake spec' 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.3 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'problem_details' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/problem_details.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'problem_details/document' 4 | require 'problem_details/version' 5 | 6 | module ProblemDetails 7 | end 8 | -------------------------------------------------------------------------------- /lib/problem_details/document.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'rack/utils' 5 | 6 | # An object representing RFC 7807 Problem Details 7 | module ProblemDetails 8 | # The class that implements a Problem Details JSON object described in RFC 7807. 9 | class Document 10 | attr_accessor :type, :title, :status, :detail, :instance 11 | 12 | def initialize(params = {}) 13 | params = params.dup 14 | @type = params.delete(:type) 15 | @status = Rack::Utils.status_code(params.delete(:status)) if params.key?(:status) 16 | @title = params.delete(:title) || (@status ? ::Rack::Utils::HTTP_STATUS_CODES[@status] : nil) 17 | @detail = params.delete(:detail) 18 | @instance = params.delete(:instance) 19 | @extentions = params 20 | end 21 | 22 | def to_hash 23 | h = {} 24 | %i[type title status detail instance].each do |key| 25 | value = public_send(key) 26 | h[key] = value if value 27 | end 28 | h.merge(@extentions) 29 | end 30 | alias_method :to_h, :to_hash 31 | 32 | def to_json 33 | to_hash.to_json 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/problem_details/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProblemDetails 4 | VERSION = '0.2.3' 5 | end 6 | -------------------------------------------------------------------------------- /problem_details-rails/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /problem_details-rails/.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /problem_details-rails/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | gemspec 6 | gem 'problem_details', path: '..' 7 | -------------------------------------------------------------------------------- /problem_details-rails/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Nobuhiro Nikushi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /problem_details-rails/README.md: -------------------------------------------------------------------------------- 1 | # ProblemDetails::Rails [![Build Status](https://travis-ci.org/nikushi/problem_details.svg?branch=master)](https://travis-ci.org/nikushi/problem_details) [![Gem Version](https://badge.fury.io/rb/problem_details-rails.svg)](https://badge.fury.io/rb/problem_details-rails) 2 | 3 | This gem helps Rails systems to render "problem detail" json form described in [RFC7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807). 4 | 5 | See the detail at [nikushi/problem_details](https://github.com/nikushi/problem_details). 6 | -------------------------------------------------------------------------------- /problem_details-rails/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /problem_details-rails/lib/problem_details-rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'problem_details/rails' 4 | -------------------------------------------------------------------------------- /problem_details-rails/lib/problem_details/rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProblemDetails 4 | module Rails 5 | require 'problem_details' 6 | require_relative 'rails/config' 7 | require_relative 'rails/railtie' if defined?(::Rails) 8 | require_relative 'rails/version' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /problem_details-rails/lib/problem_details/rails/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProblemDetails 4 | module Rails 5 | # Configures global settings for ProblemDetails::Rails 6 | # ProblemDetails::Rails.configure do |config| 7 | # config.default_json_content_type = 'application/json' 8 | # end 9 | class << self 10 | def configure 11 | yield config 12 | end 13 | 14 | def config 15 | @config ||= Config.new 16 | end 17 | end 18 | 19 | class Config 20 | attr_accessor :default_json_content_type 21 | 22 | def initialize 23 | @default_json_content_type = 'application/problem+json' 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /problem_details-rails/lib/problem_details/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProblemDetails 4 | class Railtie < ::Rails::Railtie 5 | initializer 'problem_details.initialize' do |_app| 6 | ActiveSupport.on_load(:action_controller) do 7 | require 'problem_details' 8 | require 'problem_details/rails/render_option_builder' 9 | 10 | ActionController::Renderers.add :problem do |content, options| 11 | builder = ProblemDetails::Rails::RenderOptionBuilder.new(content, options) 12 | render(**builder.build) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /problem_details-rails/lib/problem_details/rails/render_option_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProblemDetails 4 | module Rails 5 | # A builder to build a hash that is passed to ActionController's render method. 6 | class RenderOptionBuilder 7 | # @param [Hash] content given as a value of :problem key of render method. 8 | # @param [Hash] options options of render method. 9 | def initialize(content, options) 10 | @content = content 11 | @options = options.dup 12 | end 13 | 14 | # @return [Hash] build a hash being passed to render method. 15 | def build 16 | content_type = @options.delete(:content_type) || ProblemDetails::Rails.config.default_json_content_type 17 | status = @options.delete(:status) || :ok 18 | document = ProblemDetails::Document.new(status: status, **@content) 19 | { json: document.to_h, status: status, content_type: content_type, **@options } 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /problem_details-rails/lib/problem_details/rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ProblemDetails 4 | module Rails 5 | VERSION = '0.2.3' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /problem_details-rails/problem_details-rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | version = File.read(File.expand_path('../VERSION', __dir__)).strip 4 | lib = File.expand_path('lib', __dir__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'problem_details-rails' 9 | spec.version = version 10 | spec.authors = ['Nobuhiro Nikushi'] 11 | spec.email = ['deneb.ge@gmail.com'] 12 | spec.summary = 'Add a :problem renderer that renponds with RFC 7807 Problem Details format' 13 | spec.description = spec.summary 14 | spec.homepage = 'https://github.com/nikushi/problem_details' 15 | spec.license = 'MIT' 16 | spec.files = Dir['lib/**/*.rb'] + %w[ 17 | LICENSE.txt 18 | README.md 19 | Rakefile 20 | problem_details-rails.gemspec 21 | ] 22 | spec.require_paths = ['lib'] 23 | 24 | spec.required_ruby_version = '>= 2.0.0' 25 | 26 | spec.add_runtime_dependency 'problem_details', version 27 | spec.add_dependency 'actionpack', '>= 4.0' 28 | spec.add_dependency 'activesupport', '>= 4.0' 29 | spec.add_development_dependency 'bundler', '>= 1.16' 30 | spec.add_development_dependency 'rake', '~> 10.0' 31 | spec.add_development_dependency 'rspec', '~> 3.0' 32 | end 33 | -------------------------------------------------------------------------------- /problem_details-rails/spec/problem_details/rails/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe ProblemDetails::Rails::Config do 6 | describe 'default_json_content_type' do 7 | subject { ProblemDetails::Rails.config.default_json_content_type } 8 | 9 | context 'by default' do 10 | it { is_expected.to eq 'application/problem+json' } 11 | end 12 | context 'configure via a config block' do 13 | before do 14 | ProblemDetails::Rails.configure { |c| c.default_json_content_type = 'application/json' } 15 | end 16 | after do 17 | ProblemDetails::Rails.configure { |c| c.default_json_content_type = 'application/problem+json' } 18 | end 19 | it { is_expected.to eq 'application/json' } 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /problem_details-rails/spec/problem_details/rails/render_option_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'problem_details/rails/render_option_builder' 5 | 6 | RSpec.describe ProblemDetails::Rails::RenderOptionBuilder do 7 | describe '#build' do 8 | subject { described_class.new(content, options).build } 9 | 10 | context 'example 1 in RFC' do 11 | let(:content) do 12 | { 13 | type: 'https://example.com/probs/out-of-credit', 14 | title: 'You do not have enough credit.', 15 | detail: 'Your current balance is 30, but that costs 50.', 16 | instance: '/account/12345/msgs/abc', 17 | balance: 30, 18 | accounts: ['/account/12345', '/account/67890'], 19 | } 20 | end 21 | let(:options) do 22 | { 23 | status: 403, 24 | } 25 | end 26 | let(:expected) do 27 | { 28 | json: { 29 | type: 'https://example.com/probs/out-of-credit', 30 | title: 'You do not have enough credit.', 31 | status: 403, 32 | detail: 'Your current balance is 30, but that costs 50.', 33 | instance: '/account/12345/msgs/abc', 34 | balance: 30, 35 | accounts: ['/account/12345', '/account/67890'], 36 | }, 37 | status: 403, 38 | content_type: 'application/problem+json', 39 | } 40 | end 41 | 42 | it { is_expected.to eq expected } 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /problem_details-rails/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'problem_details' 5 | require 'problem_details/rails' 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = '.rspec_status' 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /problem_details.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | version = File.read(File.expand_path('VERSION', __dir__)).strip 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'problem_details' 8 | spec.version = version 9 | spec.authors = ['Nobuhiro Nikushi'] 10 | spec.email = ['deneb.ge@gmail.com'] 11 | spec.summary = 'An implementation of RFC 7807 Problem Details for HTTP APIs' 12 | spec.description = spec.summary 13 | spec.homepage = 'https://github.com/nikushi/problem_details' 14 | spec.license = 'MIT' 15 | spec.files = Dir['lib/**/*.rb'] + %w[ 16 | CHANGELOG.md 17 | LICENSE.txt 18 | README.md 19 | Rakefile 20 | problem_details.gemspec 21 | VERSION 22 | ] 23 | spec.require_paths = ['lib'] 24 | 25 | spec.required_ruby_version = '>= 2.0.0' 26 | 27 | spec.add_runtime_dependency 'rack', '>= 1.1.0' 28 | spec.add_development_dependency 'bundler', '>= 1.16' 29 | spec.add_development_dependency 'rake', '~> 10.0' 30 | spec.add_development_dependency 'rspec', '~> 3.0' 31 | end 32 | -------------------------------------------------------------------------------- /sinatra-problem_details/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /sinatra-problem_details/.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /sinatra-problem_details/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | gemspec 6 | gem 'problem_details', path: '..' 7 | -------------------------------------------------------------------------------- /sinatra-problem_details/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Nobuhiro Nikushi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /sinatra-problem_details/README.md: -------------------------------------------------------------------------------- 1 | # Sinatra::ProblemDetails [![Build Status](https://travis-ci.org/nikushi/problem_details.svg?branch=master)](https://travis-ci.org/nikushi/problem_details) [![Gem Version](https://badge.fury.io/rb/sinatra-problem_details.svg)](https://badge.fury.io/rb/sinatra-problem_details) 2 | 3 | Sinatra::ProblemDetails adds a helper method, called `problem`, for "problem detail" json response, 4 | described in [RFC7807 Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807). 5 | 6 | See the detail at [nikushi/problem_details](https://github.com/nikushi/problem_details). 7 | -------------------------------------------------------------------------------- /sinatra-problem_details/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /sinatra-problem_details/lib/sinatra-problem_details.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sinatra/problem_details' 4 | -------------------------------------------------------------------------------- /sinatra-problem_details/lib/sinatra/problem_details.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'problem_details/version' 4 | 5 | require 'sinatra/base' 6 | require 'sinatra/json' 7 | require 'problem_details' 8 | 9 | module Sinatra 10 | module ProblemDetails 11 | module Helpers 12 | def problem(object, options = {}) 13 | options = { content_type: settings.problem_json_content_type }.merge(options) 14 | document = ::ProblemDetails::Document.new(status: status, **object) 15 | json(document.to_h, options) 16 | end 17 | end 18 | 19 | def self.registered(base) 20 | base.set :problem_json_content_type, 'application/problem+json' 21 | base.helpers Helpers 22 | end 23 | end 24 | 25 | register ProblemDetails 26 | end 27 | -------------------------------------------------------------------------------- /sinatra-problem_details/lib/sinatra/problem_details/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Sinatra 4 | module ProblemDetails 5 | VERSION = '0.2.3' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /sinatra-problem_details/sinatra-problem_details.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | version = File.read(File.expand_path('../VERSION', __dir__)).strip 4 | lib = File.expand_path('lib', __dir__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'sinatra-problem_details' 9 | spec.version = version 10 | spec.authors = ['Nobuhiro Nikushi'] 11 | spec.email = ['deneb.ge@gmail.com'] 12 | spec.summary = 'Sinatra extention to add +problem+ method to respond with RFC 7807 Problem Details form' 13 | spec.description = spec.summary 14 | spec.homepage = 'https://github.com/nikushi/problem_details' 15 | spec.license = 'MIT' 16 | spec.files = Dir['lib/**/*.rb'] + %w[ 17 | LICENSE.txt 18 | README.md 19 | Rakefile 20 | sinatra-problem_details.gemspec 21 | ] 22 | spec.require_paths = ['lib'] 23 | 24 | spec.required_ruby_version = '>= 2.0.0' 25 | 26 | spec.add_runtime_dependency 'problem_details', version 27 | spec.add_runtime_dependency 'sinatra-contrib', '>= 1.4.1' 28 | spec.add_development_dependency 'bundler', '>= 1.16' 29 | spec.add_development_dependency 'rake', '~> 10.0' 30 | spec.add_development_dependency 'rspec', '~> 3.0' 31 | spec.add_development_dependency 'rack-test' 32 | end 33 | -------------------------------------------------------------------------------- /sinatra-problem_details/spec/problem_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'json' 5 | 6 | RSpec.describe Sinatra::ProblemDetails do 7 | def mock_app(&block) 8 | super do 9 | register Sinatra::ProblemDetails 10 | class_eval(&block) 11 | end 12 | end 13 | 14 | def results_in(obj) 15 | expect(JSON.parse(get('/').body)).to eq(obj) 16 | end 17 | 18 | it "encodes objects with default status" do 19 | mock_app { get('/') { problem :foo => [1, 'bar', nil] } } 20 | results_in 'status' => 200, 'title' => 'OK', 'foo' => [1, 'bar', nil] 21 | end 22 | 23 | it "encodes objects with given status" do 24 | mock_app do 25 | get('/') do 26 | status 404 27 | problem :foo => [1, 'bar', nil] 28 | end 29 | end 30 | results_in 'status' => 404, 'title' => 'Not Found', 'foo' => [1, 'bar', nil] 31 | end 32 | 33 | it "encodes objects with status and other properties including reserved ones in RFC" do 34 | mock_app do 35 | get('/') do 36 | status 403 37 | problem( 38 | type: 'https://example.com/probs/out-of-credit', 39 | title: 'You do not have enough credit.', 40 | detail: 'Your current balance is 30, but that costs 50.', 41 | instance: '/account/12345/msgs/abc', 42 | balance: 30, 43 | accounts: ['/account/12345', '/account/67890'], 44 | ) 45 | end 46 | end 47 | results_in( 48 | 'status' => 403, 49 | 'type' => 'https://example.com/probs/out-of-credit', 50 | 'title' => 'You do not have enough credit.', 51 | 'detail' => 'Your current balance is 30, but that costs 50.', 52 | 'instance' => '/account/12345/msgs/abc', 53 | 'balance' => 30, 54 | 'accounts' => ['/account/12345', '/account/67890'], 55 | ) 56 | end 57 | 58 | it "sets the content type to 'application/problem+json'" do 59 | mock_app { get('/') { problem({}) } } 60 | expect(get('/')["Content-Type"]).to include("application/problem+json") 61 | end 62 | 63 | it "allows overriding content type with :content_type" do 64 | mock_app { get('/') { json({}, :content_type => "application/json") } } 65 | expect(get('/')["Content-Type"]).to eq("application/json") 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /sinatra-problem_details/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RACK_ENV'] = 'test' 4 | require 'bundler/setup' 5 | 6 | require 'sinatra/contrib' 7 | require 'sinatra-problem_details' 8 | 9 | RSpec.configure do |config| 10 | # Enable flags like --only-failures and --next-failure 11 | config.example_status_persistence_file_path = '.rspec_status' 12 | 13 | # Disable RSpec exposing methods globally on `Module` and `main` 14 | config.disable_monkey_patching! 15 | 16 | config.expect_with :rspec do |c| 17 | c.syntax = :expect 18 | end 19 | 20 | config.include Sinatra::TestHelpers 21 | end 22 | -------------------------------------------------------------------------------- /spec/problem_details/document_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe ProblemDetails::Document do 6 | describe '.new' do 7 | subject { described_class.new(params) } 8 | 9 | let(:params) { {} } 10 | 11 | context 'without any params' do 12 | it { is_expected.to be_a described_class } 13 | end 14 | 15 | context 'without type' do 16 | it { expect(subject.type).to eq nil } 17 | end 18 | 19 | context 'with title' do 20 | let(:params) { { title: 'foo' } } 21 | 22 | it { expect(subject.title).to eq 'foo' } 23 | end 24 | 25 | context 'with status' do 26 | let(:params) { { status: 400 } } 27 | 28 | it { expect(subject.title).to eq 'Bad Request' } 29 | it { expect(subject.status).to eq 400 } 30 | end 31 | 32 | context 'with symbolized status' do 33 | let(:params) { { status: :bad_request } } 34 | 35 | it { expect(subject.title).to eq 'Bad Request' } 36 | it { expect(subject.status).to eq 400 } 37 | end 38 | 39 | context 'without status and title' do 40 | it { expect(subject.title).to be_nil } 41 | it { expect(subject.status).to be_nil } 42 | end 43 | end 44 | 45 | describe '#to_json' do 46 | subject { described_class.new(params).to_json } 47 | 48 | let(:params) do 49 | { 50 | type: 'https://example.com/probs/out-of-credit', 51 | title: 'You do not have enough credit.', 52 | detail: 'Your current balance is 30, but that costs 50.', 53 | instance: '/account/12345/msgs/abc', 54 | balance: 30, 55 | accounts: %w[/account/12345 /account/67890], 56 | } 57 | end 58 | 59 | it { is_expected.to eq params.to_json } 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'problem_details' 5 | require 'problem_details/rails' 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = '.rspec_status' 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | end 18 | --------------------------------------------------------------------------------