Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a setting for disabling documentation to internal APIs #2233

Merged
merged 1 commit into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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