grape-security 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +70 -0
  5. data/.travis.yml +18 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +314 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +14 -0
  11. data/LICENSE +20 -0
  12. data/README.md +1777 -0
  13. data/RELEASING.md +105 -0
  14. data/Rakefile +69 -0
  15. data/UPGRADING.md +124 -0
  16. data/grape-security.gemspec +39 -0
  17. data/grape.png +0 -0
  18. data/lib/grape.rb +99 -0
  19. data/lib/grape/api.rb +646 -0
  20. data/lib/grape/cookies.rb +39 -0
  21. data/lib/grape/endpoint.rb +533 -0
  22. data/lib/grape/error_formatter/base.rb +31 -0
  23. data/lib/grape/error_formatter/json.rb +15 -0
  24. data/lib/grape/error_formatter/txt.rb +16 -0
  25. data/lib/grape/error_formatter/xml.rb +15 -0
  26. data/lib/grape/exceptions/base.rb +66 -0
  27. data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
  28. data/lib/grape/exceptions/invalid_formatter.rb +10 -0
  29. data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
  30. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
  31. data/lib/grape/exceptions/missing_mime_type.rb +10 -0
  32. data/lib/grape/exceptions/missing_option.rb +10 -0
  33. data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
  34. data/lib/grape/exceptions/unknown_options.rb +10 -0
  35. data/lib/grape/exceptions/unknown_validator.rb +10 -0
  36. data/lib/grape/exceptions/validation.rb +26 -0
  37. data/lib/grape/exceptions/validation_errors.rb +43 -0
  38. data/lib/grape/formatter/base.rb +31 -0
  39. data/lib/grape/formatter/json.rb +12 -0
  40. data/lib/grape/formatter/serializable_hash.rb +35 -0
  41. data/lib/grape/formatter/txt.rb +11 -0
  42. data/lib/grape/formatter/xml.rb +12 -0
  43. data/lib/grape/http/request.rb +26 -0
  44. data/lib/grape/locale/en.yml +32 -0
  45. data/lib/grape/middleware/auth/base.rb +30 -0
  46. data/lib/grape/middleware/auth/basic.rb +13 -0
  47. data/lib/grape/middleware/auth/digest.rb +13 -0
  48. data/lib/grape/middleware/auth/oauth2.rb +83 -0
  49. data/lib/grape/middleware/base.rb +62 -0
  50. data/lib/grape/middleware/error.rb +89 -0
  51. data/lib/grape/middleware/filter.rb +17 -0
  52. data/lib/grape/middleware/formatter.rb +150 -0
  53. data/lib/grape/middleware/globals.rb +13 -0
  54. data/lib/grape/middleware/versioner.rb +32 -0
  55. data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
  56. data/lib/grape/middleware/versioner/header.rb +132 -0
  57. data/lib/grape/middleware/versioner/param.rb +42 -0
  58. data/lib/grape/middleware/versioner/path.rb +52 -0
  59. data/lib/grape/namespace.rb +23 -0
  60. data/lib/grape/parser/base.rb +29 -0
  61. data/lib/grape/parser/json.rb +11 -0
  62. data/lib/grape/parser/xml.rb +11 -0
  63. data/lib/grape/path.rb +70 -0
  64. data/lib/grape/route.rb +27 -0
  65. data/lib/grape/util/content_types.rb +18 -0
  66. data/lib/grape/util/deep_merge.rb +23 -0
  67. data/lib/grape/util/hash_stack.rb +120 -0
  68. data/lib/grape/validations.rb +322 -0
  69. data/lib/grape/validations/coerce.rb +63 -0
  70. data/lib/grape/validations/default.rb +25 -0
  71. data/lib/grape/validations/exactly_one_of.rb +26 -0
  72. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  73. data/lib/grape/validations/presence.rb +16 -0
  74. data/lib/grape/validations/regexp.rb +12 -0
  75. data/lib/grape/validations/values.rb +23 -0
  76. data/lib/grape/version.rb +3 -0
  77. data/spec/grape/api_spec.rb +2571 -0
  78. data/spec/grape/endpoint_spec.rb +784 -0
  79. data/spec/grape/entity_spec.rb +324 -0
  80. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  81. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  82. data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
  83. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  84. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  85. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  86. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  87. data/spec/grape/middleware/auth/basic_spec.rb +31 -0
  88. data/spec/grape/middleware/auth/digest_spec.rb +47 -0
  89. data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
  90. data/spec/grape/middleware/base_spec.rb +58 -0
  91. data/spec/grape/middleware/error_spec.rb +45 -0
  92. data/spec/grape/middleware/exception_spec.rb +184 -0
  93. data/spec/grape/middleware/formatter_spec.rb +258 -0
  94. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
  95. data/spec/grape/middleware/versioner/header_spec.rb +302 -0
  96. data/spec/grape/middleware/versioner/param_spec.rb +58 -0
  97. data/spec/grape/middleware/versioner/path_spec.rb +44 -0
  98. data/spec/grape/middleware/versioner_spec.rb +22 -0
  99. data/spec/grape/path_spec.rb +229 -0
  100. data/spec/grape/util/hash_stack_spec.rb +132 -0
  101. data/spec/grape/validations/coerce_spec.rb +208 -0
  102. data/spec/grape/validations/default_spec.rb +123 -0
  103. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  104. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  105. data/spec/grape/validations/presence_spec.rb +142 -0
  106. data/spec/grape/validations/regexp_spec.rb +40 -0
  107. data/spec/grape/validations/values_spec.rb +152 -0
  108. data/spec/grape/validations/zh-CN.yml +10 -0
  109. data/spec/grape/validations_spec.rb +994 -0
  110. data/spec/shared/versioning_examples.rb +121 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/basic_auth_encode_helpers.rb +3 -0
  113. data/spec/support/content_type_helpers.rb +11 -0
  114. data/spec/support/versioned_helpers.rb +50 -0
  115. metadata +421 -0
@@ -0,0 +1,105 @@
1
+ Releasing Grape
2
+ ===============
3
+
4
+ There're no particular rules about when to release Grape. Release bug fixes frequenty, features not so frequently and breaking API changes rarely.
5
+
6
+ ### Release
7
+
8
+ Run tests, check that all tests succeed locally.
9
+
10
+ ```
11
+ bundle install
12
+ rake
13
+ ```
14
+
15
+ Check that the last build succeeded in [Travis CI](https://travis-ci.org/intridea/grape) for all supported platforms.
16
+
17
+ Those with r/w permissions to the [master Intridea repository](https://github.com/intridea/grape) generally have large Grape-based projects. Point one to Grape HEAD and run all your API tests to catch any obvious regressions.
18
+
19
+ ```
20
+ gem grape, github: 'intridea/grape'
21
+ ```
22
+
23
+ Increment the version, modify [lib/grape/version.rb](lib/grape/version.rb).
24
+
25
+ * Increment the third number if the release has bug fixes and/or very minor features, only (eg. change `0.5.1` to `0.5.2`).
26
+ * Increment the second number if the release contains major features or breaking API changes (eg. change `0.5.1` to `0.6.0`).
27
+
28
+ Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the documentation for a stable release. Remove references to the previous release of Grape. Keep the file open, you'll have to undo this change after the release.
29
+
30
+ ```
31
+ ## Stable Release
32
+
33
+ You're reading the documentation for the stable release of Grape, 0.6.0.
34
+ ```
35
+
36
+ Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version.
37
+
38
+ ```
39
+ 0.6.0 (9/16/2013)
40
+ =================
41
+ ```
42
+
43
+ Remove the line with "Your contribution here.", since there will be no more contributions to this release.
44
+
45
+ Commit your changes.
46
+
47
+ ```
48
+ git add README.md CHANGELOG.md lib/grape/version.rb
49
+ git commit -m "Preparing for release, 0.6.0."
50
+ git push origin master
51
+ ```
52
+
53
+ Release.
54
+
55
+ ```
56
+ $ rake release
57
+
58
+ grape 0.6.0 built to pkg/grape-0.6.0.gem.
59
+ Tagged v0.6.0.
60
+ Pushed git commits and tags.
61
+ Pushed grape 0.6.0 to rubygems.org.
62
+ ```
63
+
64
+ ### Prepare for the Next Version
65
+
66
+ Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the next release.
67
+
68
+ ```
69
+ ## Stable Release
70
+
71
+ You're reading the documentation for the next release of Grape, which should be 0.6.1.
72
+ The current stable release is [0.6.0](https://github.com/intridea/grape/blob/v0.6.0/README.md).
73
+ ```
74
+
75
+ Add the next release to [CHANGELOG.md](CHANGELOG.md).
76
+
77
+ ```
78
+ Next Release
79
+ ============
80
+
81
+ * Your contribution here.
82
+ ```
83
+
84
+ Comit your changes.
85
+
86
+ ```
87
+ git add CHANGELOG.md README.md
88
+ git commit -m "Preparing for next release."
89
+ git push origin master
90
+ ```
91
+
92
+ ### Make an Announcement
93
+
94
+ Make an announcement on the [ruby-grape@googlegroups.com](mailto:ruby-grape@googlegroups.com) mailing list. The general format is as follows.
95
+
96
+ ```
97
+ Grape 0.6.0 has been released.
98
+
99
+ There were 8 contributors to this release, not counting documentation.
100
+
101
+ Please note the breaking API change in ...
102
+
103
+ [copy/paste CHANGELOG here]
104
+
105
+ ```
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup :default, :test, :development
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ require 'rspec/core/rake_task'
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = 'spec/**/*_spec.rb'
10
+ end
11
+
12
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
13
+ spec.pattern = 'spec/**/*_spec.rb'
14
+ spec.rcov = true
15
+ end
16
+
17
+ task :spec
18
+
19
+ require 'rainbow/ext/string' unless String.respond_to?(:color)
20
+ require 'rubocop/rake_task'
21
+ Rubocop::RakeTask.new(:rubocop)
22
+
23
+ task default: [:rubocop, :spec]
24
+
25
+ begin
26
+ require 'yard'
27
+ DOC_FILES = ['lib/**/*.rb', 'README.md']
28
+
29
+ YARD::Rake::YardocTask.new(:doc) do |t|
30
+ t.files = DOC_FILES
31
+ end
32
+
33
+ namespace :doc do
34
+ YARD::Rake::YardocTask.new(:pages) do |t|
35
+ t.files = DOC_FILES
36
+ t.options = ['-o', '../grape.doc/docs']
37
+ end
38
+
39
+ namespace :pages do
40
+
41
+ desc "Check out gh-pages."
42
+ task :checkout do
43
+ dir = File.dirname(__FILE__) + '/../grape.doc'
44
+ unless Dir.exist?(dir)
45
+ Dir.mkdir(dir)
46
+ Dir.chdir(dir) do
47
+ system("git init")
48
+ system("git remote add origin git@github.com:intridea/grape.git")
49
+ system("git pull")
50
+ system("git checkout gh-pages")
51
+ end
52
+ end
53
+ end
54
+
55
+ desc 'Generate and publish YARD docs to GitHub pages.'
56
+ task :publish => ['doc:pages:checkout', 'doc:pages'] do
57
+ Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do
58
+ system("git checkout gh-pages")
59
+ system("git add .")
60
+ system("git add -u")
61
+ system("git commit -m 'Generating docs for version #{Grape::VERSION}.'")
62
+ system("git push origin gh-pages")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ rescue LoadError
68
+ puts "You need to install YARD."
69
+ end
@@ -0,0 +1,124 @@
1
+ Upgrading Grape
2
+ ===============
3
+
4
+ ### Upgrading to >= 0.7.0
5
+
6
+ #### Changes in Exception Handling
7
+
8
+ Assume you have the following exception classes defined.
9
+
10
+ ```ruby
11
+ class ParentError < StandardError; end
12
+ class ChildError < ParentError; end
13
+ ```
14
+
15
+ In Grape <= 0.6.1, the `rescue_from` keyword only handled the exact exception being raised. The following code would rescue `ParentError`, but not `ChildError`.
16
+
17
+ ```ruby
18
+ rescue_from ParentError do |e|
19
+ # only rescue ParentError
20
+ end
21
+ ```
22
+
23
+ This made it impossible to rescue an exception hieararchy, which is a more sensible default. In Grape 0.7.0 or newer, both `ParentError` and `ChildError` are rescued.
24
+
25
+ ```ruby
26
+ rescue_from ParentError do |e|
27
+ # rescue both ParentError and ChildError
28
+ end
29
+ ```
30
+
31
+ To only rescue the base exception class, set `rescue_subclasses: false`.
32
+
33
+ ```ruby
34
+ rescue_from ParentError, rescue_subclasses: false do |e|
35
+ # only rescue ParentError
36
+ end
37
+ ```
38
+
39
+ See [#544](https://github.com/intridea/grape/pull/544) for more information.
40
+
41
+
42
+ #### Changes in the Default HTTP Status Code
43
+
44
+ In Grape <= 0.6.1, the default status code returned from `error!` was 403.
45
+
46
+ ```ruby
47
+ error! "You may not reticulate this spline!" # yields HTTP error 403
48
+ ```
49
+
50
+ This was a bad default value, since 403 means "Forbidden". Change any call to `error!` that does not specify a status code to specify one. The new default value is a more sensible default of 500, which is "Internal Server Error".
51
+
52
+ ```ruby
53
+ error! "You may not reticulate this spline!", 403 # yields HTTP error 403
54
+ ```
55
+
56
+ You may also use `default_error_status` to change the global default.
57
+
58
+ ```ruby
59
+ default_error_status 400
60
+ ```
61
+
62
+ See [#525](https://github.com/intridea/Grape/pull/525) for more information.
63
+
64
+
65
+ #### Changes in Parameter Declaration and Validation
66
+
67
+ In Grape <= 0.6.1, `group`, `optional` and `requires` keywords with a block accepted either an `Array` or a `Hash`.
68
+
69
+ ```ruby
70
+ params do
71
+ requires :id, type: Integer
72
+ group :name do
73
+ requires :first_name
74
+ requires :last_name
75
+ end
76
+ end
77
+ ```
78
+
79
+ This caused the ambiguity and unexpected errors described in [#543](https://github.com/intridea/Grape/issues/543).
80
+
81
+ In Grape 0.7.0, the `group`, `optional` and `requires` keywords take an additional `type` attribute which defaults to `Array`. This means that without a `type` attribute, these nested parameters will no longer accept a single hash, only an array (of hashes).
82
+
83
+ Whereas in 0.6.1 the API above accepted the following json, it no longer does in 0.7.0.
84
+
85
+ ```json
86
+ {
87
+ "id": 1,
88
+ "name": {
89
+ "first_name": "John",
90
+ "last_name" : "Doe"
91
+ }
92
+ }
93
+ ```
94
+
95
+ The `params` block should now read as follows.
96
+
97
+ ```ruby
98
+ params do
99
+ requires :id, type: Integer
100
+ requires :name, type: Hash do
101
+ requires :first_name
102
+ requires :last_name
103
+ end
104
+ end
105
+ ```
106
+
107
+ See [#545](https://github.com/intridea/Grape/pull/545) for more information.
108
+
109
+
110
+ ### Upgrading to 0.6.0
111
+
112
+ In Grape <= 0.5.0, only the first validation error was raised and processing aborted. Validation errors are now collected and a single `Grape::Exceptions::ValidationErrors` exception is raised. You can access the collection of validation errors as `.errors`.
113
+
114
+ ```ruby
115
+ rescue_from Grape::Exceptions::Validations do |e|
116
+ Rack::Response.new({
117
+ status: 422,
118
+ message: e.message,
119
+ errors: e.errors
120
+ }.to_json, 422)
121
+ end
122
+ ```
123
+
124
+ For more information see [#462](https://github.com/intridea/grape/issues/462).
@@ -0,0 +1,39 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "grape/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "grape-security"
6
+ s.version = Grape::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Michael Bleigh"]
9
+ s.email = ["michael@intridea.com"]
10
+ s.homepage = "https://github.com/intridea/grape"
11
+ s.summary = %q{Backported security patched version] A simple Ruby framework for building REST-like APIs.}
12
+ s.description = %q{[Backported security patched version] A Ruby framework for rapid API development with great conventions.}
13
+ s.license = "MIT"
14
+
15
+ s.rubyforge_project = "grape"
16
+
17
+ s.add_runtime_dependency 'rack', '>= 1.3.0'
18
+ s.add_runtime_dependency 'rack-mount'
19
+ s.add_runtime_dependency 'rack-accept'
20
+ s.add_runtime_dependency 'activesupport'
21
+ s.add_runtime_dependency 'multi_json', '>= 1.3.2'
22
+ s.add_runtime_dependency 'multi_xml', '>= 0.5.2'
23
+ s.add_runtime_dependency 'hashie', '>= 2.1.0'
24
+ s.add_runtime_dependency 'virtus', '>= 1.0.0'
25
+ s.add_runtime_dependency 'builder'
26
+
27
+ s.add_development_dependency 'grape-entity', '>= 0.2.0'
28
+ s.add_development_dependency 'rake'
29
+ s.add_development_dependency 'maruku'
30
+ s.add_development_dependency 'yard'
31
+ s.add_development_dependency 'rack-test'
32
+ s.add_development_dependency 'rspec', '~> 2.9'
33
+ s.add_development_dependency 'bundler'
34
+
35
+ s.files = `git ls-files`.split("\n")
36
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
37
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
38
+ s.require_paths = ["lib"]
39
+ end
Binary file
@@ -0,0 +1,99 @@
1
+ require 'logger'
2
+ require 'rack'
3
+ require 'rack/mount'
4
+ require 'rack/builder'
5
+ require 'rack/accept'
6
+ require 'rack/auth/basic'
7
+ require 'rack/auth/digest/md5'
8
+ require 'hashie'
9
+ require 'set'
10
+ require 'active_support/core_ext/hash/indifferent_access'
11
+ require 'active_support/ordered_hash'
12
+ require 'active_support/core_ext/object/conversions'
13
+ require 'active_support/core_ext/array/extract_options'
14
+ require 'grape/util/deep_merge'
15
+ require 'grape/util/content_types'
16
+ require 'multi_json'
17
+ require 'multi_xml'
18
+ require 'virtus'
19
+ require 'i18n'
20
+ require 'thread'
21
+
22
+ I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)
23
+
24
+ module Grape
25
+ autoload :API, 'grape/api'
26
+ autoload :Endpoint, 'grape/endpoint'
27
+
28
+ autoload :Route, 'grape/route'
29
+ autoload :Namespace, 'grape/namespace'
30
+
31
+ autoload :Path, 'grape/path'
32
+
33
+ autoload :Cookies, 'grape/cookies'
34
+ autoload :Validations, 'grape/validations'
35
+ autoload :Request, 'grape/http/request'
36
+
37
+ module Exceptions
38
+ autoload :Base, 'grape/exceptions/base'
39
+ autoload :Validation, 'grape/exceptions/validation'
40
+ autoload :ValidationErrors, 'grape/exceptions/validation_errors'
41
+ autoload :MissingVendorOption, 'grape/exceptions/missing_vendor_option'
42
+ autoload :MissingMimeType, 'grape/exceptions/missing_mime_type'
43
+ autoload :MissingOption, 'grape/exceptions/missing_option'
44
+ autoload :InvalidFormatter, 'grape/exceptions/invalid_formatter'
45
+ autoload :InvalidVersionerOption, 'grape/exceptions/invalid_versioner_option'
46
+ autoload :UnknownValidator, 'grape/exceptions/unknown_validator'
47
+ autoload :UnknownOptions, 'grape/exceptions/unknown_options'
48
+ autoload :InvalidWithOptionForRepresent, 'grape/exceptions/invalid_with_option_for_represent'
49
+ autoload :IncompatibleOptionValues, 'grape/exceptions/incompatible_option_values'
50
+ end
51
+
52
+ module ErrorFormatter
53
+ autoload :Base, 'grape/error_formatter/base'
54
+ autoload :Json, 'grape/error_formatter/json'
55
+ autoload :Txt, 'grape/error_formatter/txt'
56
+ autoload :Xml, 'grape/error_formatter/xml'
57
+ end
58
+
59
+ module Formatter
60
+ autoload :Base, 'grape/formatter/base'
61
+ autoload :Json, 'grape/formatter/json'
62
+ autoload :SerializableHash, 'grape/formatter/serializable_hash'
63
+ autoload :Txt, 'grape/formatter/txt'
64
+ autoload :Xml, 'grape/formatter/xml'
65
+ end
66
+
67
+ module Parser
68
+ autoload :Base, 'grape/parser/base'
69
+ autoload :Json, 'grape/parser/json'
70
+ autoload :Xml, 'grape/parser/xml'
71
+ end
72
+
73
+ module Middleware
74
+ autoload :Base, 'grape/middleware/base'
75
+ autoload :Versioner, 'grape/middleware/versioner'
76
+ autoload :Formatter, 'grape/middleware/formatter'
77
+ autoload :Error, 'grape/middleware/error'
78
+
79
+ module Auth
80
+ autoload :OAuth2, 'grape/middleware/auth/oauth2'
81
+ autoload :Base, 'grape/middleware/auth/base'
82
+ autoload :Basic, 'grape/middleware/auth/basic'
83
+ autoload :Digest, 'grape/middleware/auth/digest'
84
+ end
85
+
86
+ module Versioner
87
+ autoload :Path, 'grape/middleware/versioner/path'
88
+ autoload :Header, 'grape/middleware/versioner/header'
89
+ autoload :Param, 'grape/middleware/versioner/param'
90
+ autoload :AcceptVersionHeader, 'grape/middleware/versioner/accept_version_header'
91
+ end
92
+ end
93
+
94
+ module Util
95
+ autoload :HashStack, 'grape/util/hash_stack'
96
+ end
97
+ end
98
+
99
+ require 'grape/version'
@@ -0,0 +1,646 @@
1
+ module Grape
2
+ # The API class is the primary entry point for
3
+ # creating Grape APIs.Users should subclass this
4
+ # class in order to build an API.
5
+ class API
6
+ extend Validations::ClassMethods
7
+
8
+ class << self
9
+ attr_reader :endpoints, :instance, :routes, :route_set, :settings, :versions
10
+ attr_writer :logger
11
+
12
+ LOCK = Mutex.new
13
+
14
+ def logger(logger = nil)
15
+ if logger
16
+ @logger = logger
17
+ else
18
+ @logger ||= Logger.new($stdout)
19
+ end
20
+ end
21
+
22
+ def reset!
23
+ @settings = Grape::Util::HashStack.new
24
+ @route_set = Rack::Mount::RouteSet.new
25
+ @endpoints = []
26
+ @routes = nil
27
+ reset_validations!
28
+ end
29
+
30
+ def compile
31
+ @instance ||= new
32
+ end
33
+
34
+ def change!
35
+ @instance = nil
36
+ end
37
+
38
+ def call(env)
39
+ LOCK.synchronize { compile } unless instance
40
+ call!(env)
41
+ end
42
+
43
+ def call!(env)
44
+ instance.call(env)
45
+ end
46
+
47
+ # Set a configuration value for this namespace.
48
+ #
49
+ # @param key [Symbol] The key of the configuration variable.
50
+ # @param value [Object] The value to which to set the configuration variable.
51
+ def set(key, value)
52
+ settings[key.to_sym] = value
53
+ end
54
+
55
+ # Add to a configuration value for this
56
+ # namespace.
57
+ #
58
+ # @param key [Symbol] The key of the configuration variable.
59
+ # @param value [Object] The value to which to set the configuration variable.
60
+ def imbue(key, value)
61
+ settings.imbue(key, value)
62
+ end
63
+
64
+ # Define a root URL prefix for your entire API.
65
+ def prefix(prefix = nil)
66
+ prefix ? set(:root_prefix, prefix) : settings[:root_prefix]
67
+ end
68
+
69
+ # Do not route HEAD requests to GET requests automatically
70
+ def do_not_route_head!
71
+ set(:do_not_route_head, true)
72
+ end
73
+
74
+ # Do not automatically route OPTIONS
75
+ def do_not_route_options!
76
+ set(:do_not_route_options, true)
77
+ end
78
+
79
+ # Specify an API version.
80
+ #
81
+ # @example API with legacy support.
82
+ # class MyAPI < Grape::API
83
+ # version 'v2'
84
+ #
85
+ # get '/main' do
86
+ # {some: 'data'}
87
+ # end
88
+ #
89
+ # version 'v1' do
90
+ # get '/main' do
91
+ # {legacy: 'data'}
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ def version(*args, &block)
97
+ if args.any?
98
+ options = args.pop if args.last.is_a? Hash
99
+ options ||= {}
100
+ options = { using: :path }.merge(options)
101
+
102
+ raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
103
+
104
+ @versions = versions | args
105
+ nest(block) do
106
+ set(:version, args)
107
+ set(:version_options, options)
108
+ end
109
+ end
110
+
111
+ @versions.last unless @versions.nil?
112
+ end
113
+
114
+ # Add a description to the next namespace or function.
115
+ def desc(description, options = {})
116
+ @last_description = options.merge(description: description)
117
+ end
118
+
119
+ # Specify the default format for the API's serializers.
120
+ # May be `:json` or `:txt` (default).
121
+ def default_format(new_format = nil)
122
+ new_format ? set(:default_format, new_format.to_sym) : settings[:default_format]
123
+ end
124
+
125
+ # Specify the format for the API's serializers.
126
+ # May be `:json`, `:xml`, `:txt`, etc.
127
+ def format(new_format = nil)
128
+ if new_format
129
+ set(:format, new_format.to_sym)
130
+ # define the default error formatters
131
+ set(:default_error_formatter, Grape::ErrorFormatter::Base.formatter_for(new_format, {}))
132
+ # define a single mime type
133
+ mime_type = content_types[new_format.to_sym]
134
+ raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type
135
+ settings.imbue(:content_types, new_format.to_sym => mime_type)
136
+ else
137
+ settings[:format]
138
+ end
139
+ end
140
+
141
+ # Specify a custom formatter for a content-type.
142
+ def formatter(content_type, new_formatter)
143
+ settings.imbue(:formatters, content_type.to_sym => new_formatter)
144
+ end
145
+
146
+ # Specify a custom parser for a content-type.
147
+ def parser(content_type, new_parser)
148
+ settings.imbue(:parsers, content_type.to_sym => new_parser)
149
+ end
150
+
151
+ # Specify a default error formatter.
152
+ def default_error_formatter(new_formatter_name = nil)
153
+ if new_formatter_name
154
+ new_formatter = Grape::ErrorFormatter::Base.formatter_for(new_formatter_name, {})
155
+ set(:default_error_formatter, new_formatter)
156
+ else
157
+ settings[:default_error_formatter]
158
+ end
159
+ end
160
+
161
+ def error_formatter(format, options)
162
+ if options.is_a?(Hash) && options.key?(:with)
163
+ formatter = options[:with]
164
+ else
165
+ formatter = options
166
+ end
167
+
168
+ settings.imbue(:error_formatters, format.to_sym => formatter)
169
+ end
170
+
171
+ # Specify additional content-types, e.g.:
172
+ # content_type :xls, 'application/vnd.ms-excel'
173
+ def content_type(key, val)
174
+ settings.imbue(:content_types, key.to_sym => val)
175
+ end
176
+
177
+ # All available content types.
178
+ def content_types
179
+ Grape::ContentTypes.content_types_for(settings[:content_types])
180
+ end
181
+
182
+ # Specify the default status code for errors.
183
+ def default_error_status(new_status = nil)
184
+ new_status ? set(:default_error_status, new_status) : settings[:default_error_status]
185
+ end
186
+
187
+ # Allows you to rescue certain exceptions that occur to return
188
+ # a grape error rather than raising all the way to the
189
+ # server level.
190
+ #
191
+ # @example Rescue from custom exceptions
192
+ # class ExampleAPI < Grape::API
193
+ # class CustomError < StandardError; end
194
+ #
195
+ # rescue_from CustomError
196
+ # end
197
+ #
198
+ # @overload rescue_from(*exception_classes, options = {})
199
+ # @param [Array] exception_classes A list of classes that you want to rescue, or
200
+ # the symbol :all to rescue from all exceptions.
201
+ # @param [Block] block Execution block to handle the given exception.
202
+ # @param [Hash] options Options for the rescue usage.
203
+ # @option options [Boolean] :backtrace Include a backtrace in the rescue response.
204
+ # @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
205
+ # @param [Proc] handler Execution proc to handle the given exception as an
206
+ # alternative to passing a block
207
+ def rescue_from(*args, &block)
208
+ if args.last.is_a?(Proc)
209
+ handler = args.pop
210
+ elsif block_given?
211
+ handler = block
212
+ end
213
+
214
+ options = args.last.is_a?(Hash) ? args.pop : {}
215
+ handler ||= proc { options[:with] } if options.key?(:with)
216
+
217
+ if args.include?(:all)
218
+ set(:rescue_all, true)
219
+ imbue :all_rescue_handler, handler
220
+ else
221
+ handler_type =
222
+ case options[:rescue_subclasses]
223
+ when nil, true
224
+ :rescue_handlers
225
+ else
226
+ :base_only_rescue_handlers
227
+ end
228
+
229
+ imbue handler_type, Hash[args.map { |arg| [arg, handler] }]
230
+ end
231
+
232
+ imbue(:rescue_options, options)
233
+ end
234
+
235
+ # Allows you to specify a default representation entity for a
236
+ # class. This allows you to map your models to their respective
237
+ # entities once and then simply call `present` with the model.
238
+ #
239
+ # @example
240
+ # class ExampleAPI < Grape::API
241
+ # represent User, with: Entity::User
242
+ #
243
+ # get '/me' do
244
+ # present current_user # with: Entity::User is assumed
245
+ # end
246
+ # end
247
+ #
248
+ # Note that Grape will automatically go up the class ancestry to
249
+ # try to find a representing entity, so if you, for example, define
250
+ # an entity to represent `Object` then all presented objects will
251
+ # bubble up and utilize the entity provided on that `represent` call.
252
+ #
253
+ # @param model_class [Class] The model class that will be represented.
254
+ # @option options [Class] :with The entity class that will represent the model.
255
+ def represent(model_class, options)
256
+ raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with] && options[:with].is_a?(Class)
257
+ imbue(:representations, model_class => options[:with])
258
+ end
259
+
260
+ # Add helper methods that will be accessible from any
261
+ # endpoint within this namespace (and child namespaces).
262
+ #
263
+ # When called without a block, all known helpers within this scope
264
+ # are included.
265
+ #
266
+ # @param [Module] new_mod optional module of methods to include
267
+ # @param [Block] block optional block of methods to include
268
+ #
269
+ # @example Define some helpers.
270
+ #
271
+ # class ExampleAPI < Grape::API
272
+ # helpers do
273
+ # def current_user
274
+ # User.find_by_id(params[:token])
275
+ # end
276
+ # end
277
+ # end
278
+ #
279
+ def helpers(new_mod = nil, &block)
280
+ if block_given? || new_mod
281
+ mod = settings.peek[:helpers] || Module.new
282
+ if new_mod
283
+ inject_api_helpers_to_mod(new_mod) if new_mod.is_a?(Helpers)
284
+ mod.class_eval do
285
+ include new_mod
286
+ end
287
+ end
288
+ if block_given?
289
+ inject_api_helpers_to_mod(mod) do
290
+ mod.class_eval(&block)
291
+ end
292
+ end
293
+ set(:helpers, mod)
294
+ else
295
+ mod = Module.new
296
+ settings.stack.each do |s|
297
+ mod.send :include, s[:helpers] if s[:helpers]
298
+ end
299
+ change!
300
+ mod
301
+ end
302
+ end
303
+
304
+ # Add an authentication type to the API. Currently
305
+ # only `:http_basic`, `:http_digest` and `:oauth2` are supported.
306
+ def auth(type = nil, options = {}, &block)
307
+ if type
308
+ set(:auth, { type: type.to_sym, proc: block }.merge(options))
309
+ else
310
+ settings[:auth]
311
+ end
312
+ end
313
+
314
+ # Add HTTP Basic authorization to the API.
315
+ #
316
+ # @param [Hash] options A hash of options.
317
+ # @option options [String] :realm "API Authorization" The HTTP Basic realm.
318
+ def http_basic(options = {}, &block)
319
+ options[:realm] ||= "API Authorization"
320
+ auth :http_basic, options, &block
321
+ end
322
+
323
+ def http_digest(options = {}, &block)
324
+ options[:realm] ||= "API Authorization"
325
+ options[:opaque] ||= "secret"
326
+ auth :http_digest, options, &block
327
+ end
328
+
329
+ def mount(mounts)
330
+ mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
331
+ mounts.each_pair do |app, path|
332
+ if app.respond_to?(:inherit_settings, true)
333
+ app_settings = settings.clone
334
+ mount_path = Rack::Mount::Utils.normalize_path([settings[:mount_path], path].compact.join("/"))
335
+ app_settings.set :mount_path, mount_path
336
+ app.inherit_settings(app_settings)
337
+ end
338
+ endpoints << Grape::Endpoint.new(
339
+ settings.clone,
340
+ method: :any,
341
+ path: path,
342
+ app: app
343
+ )
344
+ end
345
+ end
346
+
347
+ # Defines a route that will be recognized
348
+ # by the Grape API.
349
+ #
350
+ # @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
351
+ # @param paths [String] One or more strings representing the URL segment(s) for this route.
352
+ #
353
+ # @example Defining a basic route.
354
+ # class MyAPI < Grape::API
355
+ # route(:any, '/hello') do
356
+ # {hello: 'world'}
357
+ # end
358
+ # end
359
+ def route(methods, paths = ['/'], route_options = {}, &block)
360
+ endpoint_options = {
361
+ method: methods,
362
+ path: paths,
363
+ route_options: (@namespace_description || {}).deep_merge(@last_description || {}).deep_merge(route_options || {})
364
+ }
365
+ endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)
366
+
367
+ @last_description = nil
368
+ reset_validations!
369
+ end
370
+
371
+ def before(&block)
372
+ imbue(:befores, [block])
373
+ end
374
+
375
+ def before_validation(&block)
376
+ imbue(:before_validations, [block])
377
+ end
378
+
379
+ def after_validation(&block)
380
+ imbue(:after_validations, [block])
381
+ end
382
+
383
+ def after(&block)
384
+ imbue(:afters, [block])
385
+ end
386
+
387
+ def get(paths = ['/'], options = {}, &block)
388
+ route('GET', paths, options, &block)
389
+ end
390
+
391
+ def post(paths = ['/'], options = {}, &block)
392
+ route('POST', paths, options, &block)
393
+ end
394
+
395
+ def put(paths = ['/'], options = {}, &block)
396
+ route('PUT', paths, options, &block)
397
+ end
398
+
399
+ def head(paths = ['/'], options = {}, &block)
400
+ route('HEAD', paths, options, &block)
401
+ end
402
+
403
+ def delete(paths = ['/'], options = {}, &block)
404
+ route('DELETE', paths, options, &block)
405
+ end
406
+
407
+ def options(paths = ['/'], options = {}, &block)
408
+ route('OPTIONS', paths, options, &block)
409
+ end
410
+
411
+ def patch(paths = ['/'], options = {}, &block)
412
+ route('PATCH', paths, options, &block)
413
+ end
414
+
415
+ def namespace(space = nil, options = {}, &block)
416
+ if space || block_given?
417
+ previous_namespace_description = @namespace_description
418
+ @namespace_description = (@namespace_description || {}).deep_merge(@last_description || {})
419
+ @last_description = nil
420
+ nest(block) do
421
+ set(:namespace, Namespace.new(space, options)) if space
422
+ end
423
+ @namespace_description = previous_namespace_description
424
+ else
425
+ Namespace.joined_space_path(settings)
426
+ end
427
+ end
428
+
429
+ # Thie method allows you to quickly define a parameter route segment
430
+ # in your API.
431
+ #
432
+ # @param param [Symbol] The name of the parameter you wish to declare.
433
+ # @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
434
+ def route_param(param, options = {}, &block)
435
+ options = options.dup
436
+ options[:requirements] = { param.to_sym => options[:requirements] } if options[:requirements].is_a?(Regexp)
437
+ namespace(":#{param}", options, &block)
438
+ end
439
+
440
+ alias_method :group, :namespace
441
+ alias_method :resource, :namespace
442
+ alias_method :resources, :namespace
443
+ alias_method :segment, :namespace
444
+
445
+ # Create a scope without affecting the URL.
446
+ #
447
+ # @param name [Symbol] Purely placebo, just allows to to name the scope to make the code more readable.
448
+ def scope(name = nil, &block)
449
+ nest(block)
450
+ end
451
+
452
+ # Apply a custom middleware to the API. Applies
453
+ # to the current namespace and any children, but
454
+ # not parents.
455
+ #
456
+ # @param middleware_class [Class] The class of the middleware you'd like
457
+ # to inject.
458
+ def use(middleware_class, *args, &block)
459
+ arr = [middleware_class, *args]
460
+ arr << block if block_given?
461
+ imbue(:middleware, [arr])
462
+ end
463
+
464
+ # Retrieve an array of the middleware classes
465
+ # and arguments that are currently applied to the
466
+ # application.
467
+ def middleware
468
+ settings.stack.inject([]) do |a, s|
469
+ a += s[:middleware] if s[:middleware]
470
+ a
471
+ end
472
+ end
473
+
474
+ # An array of API routes.
475
+ def routes
476
+ @routes ||= prepare_routes
477
+ end
478
+
479
+ def versions
480
+ @versions ||= []
481
+ end
482
+
483
+ def cascade(value = nil)
484
+ if value.nil?
485
+ settings.key?(:cascade) ? !!settings[:cascade] : true
486
+ else
487
+ set(:cascade, value)
488
+ end
489
+ end
490
+
491
+ protected
492
+
493
+ def prepare_routes
494
+ routes = []
495
+ endpoints.each do |endpoint|
496
+ routes.concat(endpoint.routes)
497
+ end
498
+ routes
499
+ end
500
+
501
+ # Execute first the provided block, then each of the
502
+ # block passed in. Allows for simple 'before' setups
503
+ # of settings stack pushes.
504
+ def nest(*blocks, &block)
505
+ blocks.reject! { |b| b.nil? }
506
+ if blocks.any?
507
+ settings.push # create a new context to eval the follow
508
+ instance_eval(&block) if block_given?
509
+ blocks.each { |b| instance_eval(&b) }
510
+ settings.pop # when finished, we pop the context
511
+ reset_validations!
512
+ else
513
+ instance_eval(&block)
514
+ end
515
+ end
516
+
517
+ def inherited(subclass)
518
+ subclass.reset!
519
+ subclass.logger = logger.clone
520
+ end
521
+
522
+ def inherit_settings(other_stack)
523
+ settings.prepend other_stack
524
+ endpoints.each do |e|
525
+ e.settings.prepend(other_stack)
526
+ e.options[:app].inherit_settings(other_stack) if e.options[:app].respond_to?(:inherit_settings, true)
527
+ end
528
+ end
529
+
530
+ def inject_api_helpers_to_mod(mod, &block)
531
+ mod.extend(Helpers)
532
+ yield if block_given?
533
+ mod.api_changed(self)
534
+ end
535
+ end
536
+
537
+ def initialize
538
+ @route_set = Rack::Mount::RouteSet.new
539
+ add_head_not_allowed_methods_and_options_methods
540
+ self.class.endpoints.each do |endpoint|
541
+ endpoint.mount_in(@route_set)
542
+ end
543
+ @route_set.freeze
544
+ end
545
+
546
+ def call(env)
547
+ status, headers, body = @route_set.call(env)
548
+ headers.delete('X-Cascade') unless cascade?
549
+ [status, headers, body]
550
+ end
551
+
552
+ # Some requests may return a HTTP 404 error if grape cannot find a matching
553
+ # route. In this case, Rack::Mount adds a X-Cascade header to the response
554
+ # and sets it to 'pass', indicating to grape's parents they should keep
555
+ # looking for a matching route on other resources.
556
+ #
557
+ # In some applications (e.g. mounting grape on rails), one might need to trap
558
+ # errors from reaching upstream. This is effectivelly done by unsetting
559
+ # X-Cascade. Default :cascade is true.
560
+ def cascade?
561
+ return !!self.class.settings[:cascade] if self.class.settings.key?(:cascade)
562
+ return !!self.class.settings[:version_options][:cascade] if self.class.settings[:version_options] && self.class.settings[:version_options].key?(:cascade)
563
+ true
564
+ end
565
+
566
+ reset!
567
+
568
+ private
569
+
570
+ # For every resource add a 'OPTIONS' route that returns an HTTP 204 response
571
+ # with a list of HTTP methods that can be called. Also add a route that
572
+ # will return an HTTP 405 response for any HTTP method that the resource
573
+ # cannot handle.
574
+ def add_head_not_allowed_methods_and_options_methods
575
+ methods_per_path = {}
576
+ self.class.endpoints.each do |endpoint|
577
+ routes = endpoint.routes
578
+ routes.each do |route|
579
+ methods_per_path[route.route_path] ||= []
580
+ methods_per_path[route.route_path] << route.route_method
581
+ end
582
+ end
583
+
584
+ # The paths we collected are prepared (cf. Path#prepare), so they
585
+ # contain already versioning information when using path versioning.
586
+ # Disable versioning so adding a route won't prepend versioning
587
+ # informations again.
588
+ without_versioning do
589
+ methods_per_path.each do |path, methods|
590
+ allowed_methods = methods.dup
591
+ unless self.class.settings[:do_not_route_head]
592
+ allowed_methods |= ['HEAD'] if allowed_methods.include?('GET')
593
+ end
594
+
595
+ allow_header = (['OPTIONS'] | allowed_methods).join(', ')
596
+ unless self.class.settings[:do_not_route_options]
597
+ unless allowed_methods.include?('OPTIONS')
598
+ self.class.options(path, {}) do
599
+ header 'Allow', allow_header
600
+ status 204
601
+ ''
602
+ end
603
+ end
604
+ end
605
+
606
+ not_allowed_methods = %w(GET PUT POST DELETE PATCH HEAD) - allowed_methods
607
+ not_allowed_methods << 'OPTIONS' if self.class.settings[:do_not_route_options]
608
+ self.class.route(not_allowed_methods, path) do
609
+ header 'Allow', allow_header
610
+ status 405
611
+ ''
612
+ end
613
+ end
614
+ end
615
+ end
616
+
617
+ def without_versioning(&block)
618
+ self.class.settings.push(version: nil, version_options: nil)
619
+ yield
620
+ self.class.settings.pop
621
+ end
622
+
623
+ # This module extends user defined helpers
624
+ # to provide some API-specific functionality
625
+ module Helpers
626
+ attr_accessor :api
627
+ def params(name, &block)
628
+ @named_params ||= {}
629
+ @named_params.merge! name => block
630
+ end
631
+
632
+ def api_changed(new_api)
633
+ @api = new_api
634
+ process_named_params
635
+ end
636
+
637
+ protected
638
+
639
+ def process_named_params
640
+ if @named_params && @named_params.any?
641
+ api.imbue(:named_params, @named_params)
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end