Skip to content

Commit

Permalink
a setting for disabling documentation to internal APIs
Browse files Browse the repository at this point in the history
Our application has 3 APIs, 2 of them are internal and don't require
documentations. However, there has not been any option to prevent
Grape from documenting parameters for internal APIs.

This change adds a `do_not_document!` setting which instructs Grape
to not document parameters, thus needless objects allocation is avoided.

    class Api < Grape::API
      do_not_document!
    end

The logic for documenting parameters was moved to a separate class,
the `Grape::Validations::ParamsScope` class has to many responsibilities,
it would be better to split it.
  • Loading branch information
dnesteryuk committed Jan 9, 2022
1 parent 6a10a1e commit 1c65d8b
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 144 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#### Features

* [#2233](https://github.com/ruby-grape/grape/pull/2233): A setting for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk).
* Your contribution here.

#### Fixes
Expand All @@ -13,8 +14,6 @@

### 1.6.2 (2021/12/30)

#### Features

#### Fixes

* [#2219](https://github.com/ruby-grape/grape/pull/2219): Revert the changes for autoloading provided in 1.6.1 - [@dm1try](https://github.com/dm1try).
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2249,6 +2249,18 @@ params do
end
```

If documentation isn't needed (for instance, it is an internal API), documentation can be disabled.

```ruby
class API < Grape::API
do_not_document!

# endpoints...
end
```

In this case, Grape won't create objects related to documentation which are retained in RAM forever.

## Cookies

You can set, get and delete your cookies very simply using `cookies` method.
Expand Down
4 changes: 4 additions & 0 deletions lib/grape/dsl/routing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def do_not_route_options!
namespace_inheritable(:do_not_route_options, true)
end

def do_not_document!
namespace_inheritable(:do_not_document, true)
end

def mount(mounts, *opts)
mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
mounts.each_pair do |app, path|
Expand Down
6 changes: 0 additions & 6 deletions lib/grape/dsl/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ def reset_validations!
def params(&block)
Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
end

def document_attribute(names, opts)
Array(names).each do |name|
namespace_stackable(:params, name[:full_name].to_s => opts)
end
end
end
end
end
Expand Down
58 changes: 58 additions & 0 deletions lib/grape/validations/attributes_doc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Grape
module Validations
class ParamsScope
# Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an
# internal API), the class only cleans up attributes to avoid junk in RAM.
class AttributesDoc
attr_accessor :type, :values

# @param api [Grape::API::Instance]
# @param scope [Validations::ParamsScope]
def initialize(api, scope)
@api = api
@scope = scope
@type = type
end

def extract_details(validations)
details[:required] = validations.key?(:presence)

desc = validations.delete(:desc) || validations.delete(:description)

details[:desc] = desc if desc

documentation = validations.delete(:documentation)

details[:documentation] = documentation if documentation

details[:default] = validations[:default] if validations.key?(:default)
end

def document(attrs)
return if @api.namespace_inheritable(:do_not_document)

details[:type] = type.to_s if type
details[:values] = values if values

documented_attrs = attrs.each_with_object({}) do |name, memo|
memo[@scope.full_name(name)] = details
end

@api.namespace_stackable(:params, documented_attrs)
end

def required
details[:required]
end

protected

def details
@details ||= {}
end
end
end
end
end
52 changes: 20 additions & 32 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative 'attributes_doc'

module Grape
module Validations
class ParamsScope
Expand Down Expand Up @@ -31,7 +33,7 @@ def initialize(opts, &block)
@api = opts[:api]
@optional = opts[:optional] || false
@type = opts[:type]
@group = opts[:group] || {}
@group = opts[:group]
@dependent_on = opts[:dependent_on]
@declared_params = []
@index = nil
Expand Down Expand Up @@ -269,17 +271,14 @@ def configure_declared_params
end

def validates(attrs, validations)
doc_attrs = { required: validations.key?(:presence) }
doc = AttributesDoc.new @api, self
doc.extract_details validations

coerce_type = infer_coercion(validations)

doc_attrs[:type] = coerce_type.to_s if coerce_type

desc = validations.delete(:desc) || validations.delete(:description)
doc_attrs[:desc] = desc if desc
doc.type = coerce_type

default = validations[:default]
doc_attrs[:default] = default if validations.key?(:default)

if (values_hash = validations[:values]).is_a? Hash
values = values_hash[:value]
Expand All @@ -288,7 +287,8 @@ def validates(attrs, validations)
else
values = validations[:values]
end
doc_attrs[:values] = values if values

doc.values = values

except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]

Expand All @@ -304,28 +304,22 @@ def validates(attrs, validations)
# type should be compatible with values array, if both exist
validate_value_coercion(coerce_type, values, except_values, excepts)

doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)

document_attribute(attrs, doc_attrs)
doc.document attrs

opts = derive_validator_options(validations)

order_specific_validations = Set[:as]

# Validate for presence before any other validators
validates_presence(validations, attrs, doc_attrs, opts) do |validation_type|
order_specific_validations << validation_type
end
validates_presence(validations, attrs, doc, opts)

# Before we run the rest of the validators, let's handle
# whatever coercion so that we are working with correctly
# type casted values
coerce_type validations, attrs, doc_attrs, opts
coerce_type validations, attrs, doc, opts

validations.each do |type, options|
next if order_specific_validations.include?(type)
next if type == :as

validate(type, options, attrs, doc_attrs, opts)
validate(type, options, attrs, doc, opts)
end
end

Expand Down Expand Up @@ -389,7 +383,7 @@ def check_coerce_with(validations)
# composited from more than one +requires+/+optional+
# parameter, and needs to be run before most other
# validations.
def coerce_type(validations, attrs, doc_attrs, opts)
def coerce_type(validations, attrs, doc, opts)
check_coerce_with(validations)

return unless validations.key?(:coerce)
Expand All @@ -399,7 +393,7 @@ def coerce_type(validations, attrs, doc_attrs, opts)
method: validations[:coerce_with],
message: validations[:coerce_message]
}
validate('coerce', coerce_options, attrs, doc_attrs, opts)
validate('coerce', coerce_options, attrs, doc, opts)
validations.delete(:coerce_with)
validations.delete(:coerce)
validations.delete(:coerce_message)
Expand Down Expand Up @@ -430,11 +424,11 @@ def check_incompatible_option_values(default, values, except_values, excepts)
unless Array(default).none? { |def_val| excepts.include?(def_val) }
end

def validate(type, options, attrs, doc_attrs, opts)
def validate(type, options, attrs, doc, opts)
validator_options = {
attributes: attrs,
options: options,
required: doc_attrs[:required],
required: doc.required,
params_scope: self,
opts: opts,
validator_class: Validations.require_validator(type)
Expand Down Expand Up @@ -481,17 +475,11 @@ def derive_validator_options(validations)
}
end

def validates_presence(validations, attrs, doc_attrs, opts)
def validates_presence(validations, attrs, doc, opts)
return unless validations.key?(:presence) && validations[:presence]

validate(:presence, validations[:presence], attrs, doc_attrs, opts)
yield :presence
yield :message if validations.key?(:message)
end

def document_attribute(attrs, doc_attrs)
full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
@api.document_attribute(full_attrs, doc_attrs)
validate(:presence, validations.delete(:presence), attrs, doc, opts)
validations.delete(:message) if validations.key?(:message)
end
end
end
Expand Down
59 changes: 59 additions & 0 deletions spec/grape/api/documentation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require 'spec_helper'

describe Grape::API do
subject { Class.new(described_class) }

let(:app) { subject }

context 'an endpoint with documentation' do
it 'documents parameters' do
subject.params do
requires 'price', type: Float, desc: 'Sales price'
end
subject.get '/'

expect(subject.routes.first.params['price']).to eq(required: true,
type: 'Float',
desc: 'Sales price')
end

it 'allows documentation with a hash' do
documentation = { example: 'Joe' }

subject.params do
requires 'first_name', documentation: documentation
end
subject.get '/'

expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation)
end
end

context 'an endpoint without documentation' do
before do
subject.do_not_document!

subject.params do
requires :city, type: String, desc: 'Should be ignored'
optional :postal_code, type: Integer
end
subject.post '/' do
declared(params).to_json
end
end

it 'does not document parameters for the endpoint' do
expect(subject.routes.first.params).to eq({})
end

it 'still declares params internally' do
data = { city: 'Berlin', postal_code: 10_115 }

post '/', data

expect(last_response.body).to eq(data.to_json)
end
end
end
10 changes: 0 additions & 10 deletions spec/grape/dsl/validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,6 @@ class Dummy
expect { subject.params { raise 'foo' } }.to raise_error RuntimeError, 'foo'
end
end

describe '.document_attribute' do
before do
subject.document_attribute([full_name: 'xxx'], foo: 'bar')
end

it 'creates a param documentation' do
expect(subject.namespace_stackable(:params)).to eq(['xxx' => { foo: 'bar' }])
end
end
end
end
end
Loading

0 comments on commit 1c65d8b

Please sign in to comment.