From 864c7df7cef17f66b137392e699c3a70192fea00 Mon Sep 17 00:00:00 2001 From: Vlad Wolanyk Date: Tue, 9 Sep 2025 18:59:41 -0400 Subject: [PATCH 1/6] First draft converter --- Gemfile | 3 + Gemfile.lock | 14 ++ config/initializers/rswag_api.rb | 17 ++ config/initializers/rswag_ui.rb | 17 ++ config/routes.rb | 2 + lib/grape_to_rswag.rb | 277 +++++++++++++++++++++++++++++++ spec/requests/events_spec.rb | 56 +++++++ spec/swagger_helper.rb | 43 +++++ 8 files changed, 429 insertions(+) create mode 100644 config/initializers/rswag_api.rb create mode 100644 config/initializers/rswag_ui.rb create mode 100644 lib/grape_to_rswag.rb create mode 100644 spec/requests/events_spec.rb create mode 100644 spec/swagger_helper.rb diff --git a/Gemfile b/Gemfile index bc01f3417337e9..9e3b7745fbd158 100644 --- a/Gemfile +++ b/Gemfile @@ -162,6 +162,8 @@ gem 'grape-swagger', '~> 2.1.2', group: [:development, :test], feature_category: gem 'grape-swagger-entity', '~> 0.5.5', group: [:development, :test], feature_category: :api gem 'grape-path-helpers', '~> 2.0.1', feature_category: :api gem 'rack-cors', '~> 2.0.1', require: 'rack/cors', feature_category: :shared +gem 'rswag-api', feature_category: :api +gem 'rswag-ui', feature_category: :api # GraphQL API gem 'graphql', '2.5.11', feature_category: :api @@ -516,6 +518,7 @@ group :development, :test do gem 'database_cleaner-active_record', '~> 2.2.0', feature_category: :database gem 'rspec-rails', '~> 7.1.0', feature_category: :shared + gem 'rswag-specs', feature_category: :api gem 'factory_bot_rails', '~> 6.5.0', feature_category: :tooling # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) diff --git a/Gemfile.lock b/Gemfile.lock index a0d3e8721446f5..5f9a9764cfa165 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1713,6 +1713,17 @@ GEM activerecord get_process_mem rails + rswag-api (2.16.0) + activesupport (>= 5.2, < 8.1) + railties (>= 5.2, < 8.1) + rswag-specs (2.16.0) + activesupport (>= 5.2, < 8.1) + json-schema (>= 2.2, < 6.0) + railties (>= 5.2, < 8.1) + rspec-core (>= 2.14) + rswag-ui (2.16.0) + actionpack (>= 5.2, < 8.1) + railties (>= 5.2, < 8.1) rubocop (1.71.1) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -2370,6 +2381,9 @@ DEPENDENCIES rspec-retry (~> 0.6.2) rspec_junit_formatter rspec_profiling (~> 0.0.9) + rswag-api + rswag-specs + rswag-ui rubocop ruby-lsp (~> 0.23.0) ruby-lsp-rails (~> 0.3.6) diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 00000000000000..ff064e3262a205 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# rubocop:disable all + +Rswag::Api.configure do |c| + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.openapi_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb new file mode 100644 index 00000000000000..182bcc00f7548c --- /dev/null +++ b/config/initializers/rswag_ui.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +Rswag::Ui.configure do |c| + # List the Swagger endpoints that you want to be documented through the + # swagger-ui. The first parameter is the path (absolute or relative to the UI + # host) to the corresponding endpoint and the second is a title that will be + # displayed in the document selector. + # NOTE: If you're using rspec-api to expose Swagger files + # (under openapi_root) as JSON or YAML endpoints, then the list below should + # correspond to the relative paths for those endpoints. + + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' +end diff --git a/config/routes.rb b/config/routes.rb index 151c7fa1c1b457..2de38332132032 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,8 @@ InitializerConnections.raise_if_new_database_connection do Rails.application.routes.draw do + mount Rswag::Ui::Engine => '/api-docs' + mount Rswag::Api::Engine => '/api-docs' concern :access_requestable do get :request_access, on: :collection post :request_access, on: :collection diff --git a/lib/grape_to_rswag.rb b/lib/grape_to_rswag.rb new file mode 100644 index 00000000000000..a7318010965e90 --- /dev/null +++ b/lib/grape_to_rswag.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +# rubocop:disable all + +class GrapeToRswag + attr_reader :api_class, :output_dir + + def initialize(api_class, output_dir: 'spec/requests') + @api_class = api_class + @output_dir = output_dir + end + + def convert_to_spec + routes = api_class.routes + + # Generate the spec content + content = build_spec_file(routes) + + # Derive the resource name from the API class + # This will create a filename like "users_spec.rb" for UsersAPI + resource_name = api_class.name.demodulize.underscore.gsub(/_api$/, '') + + # Write the spec to file and return the file path + file_path = write_file(resource_name, content) + + puts "Spec written to: #{file_path}" + + # Return both the content and the file path (or just the file path) + { content: content, file_path: file_path } + end + + private + + def build_spec_file(routes) + # Follows naming convention, may want to un-camelcase complex names + # this is the base of the spec + + <<~RUBY + require 'openapi_helper' + + RSpec.describe "#{api_class.name.demodulize} API", type: :request do + #{build_paths(routes)} + end + RUBY + end + + def build_paths(routes) + # need this because there can be multiple routes under the same path and need to + # be organized this way for open api spec generation + # the extra lines in the join is just for readability + + paths = routes.group_by(&:path) + + paths.map do |path, routes| + build_path_block(path, routes) + end.join("\n") + end + + def build_path_block(path, routes) + # I think we will need to replace :version with v4 in path + + <<~RUBY + + path "#{path}" do + #{routes.map { |route| build_route_test(route) }.join("\n")} + end + RUBY + end + + def build_route_test(route) + route_attrs = route.attributes + method = route_attrs.request_method.downcase + # takes care of apostrophes + description = route.description.gsub("'", "\\'") || "#{method.upcase} #{route.path}" + tags = route_attrs.tags + params = format_params(route) + + operation = [] + operation << " #{method} '#{description}' do" + operation << " tags #{tags}" + operation << " produces 'application/json'" + + operation << " consumes 'application/json'" if %w[post put patch].include?(method) + + # Add parameters + params.each do |param| + operation << format_parameter_line(param) + end + + # Add request body for POST/PUT/PATCH + if %w[post put patch].include?(method) && route.params + body_params = route.params.reject { |k, _| params.any? { |p| p[:name] == k.to_s } } + operation << build_request_body(body_params) unless body_params.empty? + end + + # Add responses + operation << "" + operation << " response '200', 'successful' do" + operation << " schema type: :object" + operation << " run_test!" + operation << " end" + operation << "" + operation << " response '404', 'not found' do" + operation << " run_test!" + operation << " end" + + operation << " end" + operation.join("\n") + end + + def build_request_body(params) + properties = params.map do |name, details| + type = grape_type_to_openapi(details[:type]) + " #{name}: { type: :#{type} }" + end.join(",\n") + + required = params.select { |_, v| v[:required] }.keys + req_str = required.empty? ? "[]" : "[#{required.map { |r| ":#{r}" }.join(', ')}]" + + <<~RUBY.chomp + + request_body_json schema: { + type: :object, + properties: { + #{properties} + }, + required: #{req_str} + } + RUBY + end + + def format_params(route) + params = [] + + # Path parameters + path_params = route.path.scan(/:(\w+)/).flatten - %w[version format] + path_params.each do |param| + param_hash = { + name: param, + in: :path, + required: true + } + + # Check if this path param has details in route.params (check both string and symbol keys) + if route.params && (route.params[param] || route.params[param.to_sym]) + details = route.params[param] || route.params[param.to_sym] + + # Get type from details + type_info = grape_type_to_openapi(details[:type]) + param_hash.merge!(type_info) + + # Add description if available + param_hash[:description] = details[:desc] if details[:desc] + + # Add example if available + if details[:documentation] && details[:documentation][:example] + param_hash[:example] = details[:documentation][:example] + end + + # Add enum values if present + param_hash[:enum] = details[:values] if details[:values] + else + # Default type if no details found + param_hash[:type] = param == 'id' ? :integer : :string + + # Default description for common params (only if no details were found) + param_hash[:description] = 'The ID of the resource' if param == 'id' + end + + params << param_hash + end + + # Query parameters (for GET requests) + if route.request_method.downcase == 'get' && route.params + route.params.each do |name, details| + # skip if already added as path param + next if params.any? { |p| p[:name] == name.to_s } + + param_hash = { + name: name.to_s, + in: :query + } + + # Merge the type information properly + type_info = grape_type_to_openapi(details[:type]) + param_hash.merge!(type_info) + + # Add description from Grape params + param_hash[:description] = details[:desc] if details[:desc] + + # Add default value if present + param_hash[:default] = details[:default] if details[:default] + + # Add enum values if present + param_hash[:enum] = details[:values] if details[:values] + + # Add example from documentation if present + if details[:documentation] && details[:documentation][:example] + param_hash[:example] = details[:documentation][:example] + end + + # Add required field + param_hash[:required] = details[:required] || false + + params << param_hash + end + end + + params + end + + def format_parameter_line(param) + # Build parameter line with proper formatting + param_options = [] + param_options << "name: #{param[:name].inspect}" + param_options << "in: #{param[:in].inspect}" + + param_options << "type: #{param[:type].inspect}" if param[:type] + + param_options << "format: #{param[:format].inspect}" if param[:format] + + # Handle array items if present + # grape uses type definitions of Array[Integer] or Array[String] which need a type for the items in the array + if param[:items] + items_formatted = param[:items].map { |k, v| "#{k}: #{v.inspect}" }.join(', ') + param_options << "items: { #{items_formatted} }" + end + + # Add description if present + if param[:description] + # Escape single quotes in description + escaped_desc = param[:description].gsub("'", "\\'") + param_options << "description: '#{escaped_desc}'" + end + + # Add default value if present + param_options << "default: #{param[:default].inspect}" if param.key?(:default) + + # Add enum values if present + param_options << "enum: #{param[:enum].inspect}" if param[:enum] + + # Add example if present + param_options << "example: #{param[:example].inspect}" if param.key?(:example) + + # Format the required field + param_options << "required: #{param[:required]}" if param.key?(:required) + + " parameter #{param_options.join(', ')}" + end + + def grape_type_to_openapi(type) + return { type: :string } unless type + + type_mapping = { + 'String' => { type: :string }, + 'Integer' => { type: :integer }, + 'Float' => { type: :number }, + 'Boolean' => { type: :boolean }, + 'Date' => { type: :string, format: :date }, + 'DateTime' => { type: :string, format: 'date-time' }, + 'Time' => { type: :string, format: 'date-time' }, + 'Array' => { type: :array }, + 'Array[String]' => { type: :array, items: { type: :string } }, + 'Array[Integer]' => { type: :array, items: { type: :integer } }, + 'Hash' => { type: :object } + } + + type_mapping[type.to_s] || { type: :string } + end + + def write_file(resource, content) + FileUtils.mkdir_p(@output_dir) + file_path = File.join(@output_dir, "#{resource}_spec.rb") + File.write(file_path, content) + file_path + end +end diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb new file mode 100644 index 00000000000000..e6e1257ad4504b --- /dev/null +++ b/spec/requests/events_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +# rubocop:disable all +require 'openapi_helper' + +RSpec.describe "Events API", type: :request do + + path "/events(.:format)" do + get 'List currently authenticated users eventss events' do + tags + produces 'application/json' + parameter name: "scope", in: :query, type: :string, description: 'Include all events across a user’s projects', example: "all", required: false + parameter name: "page", in: :query, type: :integer, description: 'Current page number', default: 1, example: 1, required: false + parameter name: "per_page", in: :query, type: :integer, description: 'Number of items per page', default: 20, example: 20, required: false + parameter name: "action", in: :query, type: :string, description: 'Event action to filter on', required: false + parameter name: "target_type", in: :query, type: :string, description: 'Event target type to filter on', enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false + parameter name: "before", in: :query, type: :string, format: :date, description: 'Include only events created before this date', required: false + parameter name: "after", in: :query, type: :string, format: :date, description: 'Include only events created after this date', required: false + parameter name: "sort", in: :query, type: :string, description: 'Return events sorted in ascending and descending order', default: "desc", enum: ["asc", "desc"], required: false + + response '200', 'successful' do + schema type: :object + run_test! + end + + response '404', 'not found' do + run_test! + end + end + end + + + path "/users/:id/events(.:format)" do + get 'Get the contribution events of a specified user' do + tags ["events"] + produces 'application/json' + parameter name: "id", in: :path, type: :string, description: 'The ID or username of the user', required: true + parameter name: "page", in: :query, type: :integer, description: 'Current page number', default: 1, example: 1, required: false + parameter name: "per_page", in: :query, type: :integer, description: 'Number of items per page', default: 20, example: 20, required: false + parameter name: "action", in: :query, type: :string, description: 'Event action to filter on', required: false + parameter name: "target_type", in: :query, type: :string, description: 'Event target type to filter on', enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false + parameter name: "before", in: :query, type: :string, format: :date, description: 'Include only events created before this date', required: false + parameter name: "after", in: :query, type: :string, format: :date, description: 'Include only events created after this date', required: false + parameter name: "sort", in: :query, type: :string, description: 'Return events sorted in ascending and descending order', default: "desc", enum: ["asc", "desc"], required: false + + response '200', 'successful' do + schema type: :object + run_test! + end + + response '404', 'not found' do + run_test! + end + end + end + +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 00000000000000..14c1214976e532 --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.openapi_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under openapi_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a openapi_spec tag to the + # the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json' + config.openapi_specs = { + 'v1/swagger.yaml' => { + openapi: '3.0.1', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ] + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The openapi_specs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.openapi_format = :yaml +end -- GitLab From dd03f5a1f0d746f42e8f5ec8df96b5ca9d1c1bb3 Mon Sep 17 00:00:00 2001 From: Vlad Wolanyk Date: Wed, 10 Sep 2025 10:43:47 -0400 Subject: [PATCH 2/6] Generate first docs --- lib/grape_to_rswag.rb | 13 +- spec/requests/events_spec.rb | 16 +- spec/support/known_rspec_metadata_keys.yml | 4 + spec/swagger_helper.rb | 9 +- swagger/v1/swagger.yaml | 179 +++++++++++++++++++++ 5 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 swagger/v1/swagger.yaml diff --git a/lib/grape_to_rswag.rb b/lib/grape_to_rswag.rb index a7318010965e90..0c57627f17cc27 100644 --- a/lib/grape_to_rswag.rb +++ b/lib/grape_to_rswag.rb @@ -36,7 +36,10 @@ def build_spec_file(routes) # this is the base of the spec <<~RUBY - require 'openapi_helper' + # frozen_string_literal: true + # rubocop:disable all + + require 'swagger_helper' RSpec.describe "#{api_class.name.demodulize} API", type: :request do #{build_paths(routes)} @@ -61,7 +64,7 @@ def build_path_block(path, routes) <<~RUBY - path "#{path}" do + path "#{format_path(path)}" do #{routes.map { |route| build_route_test(route) }.join("\n")} end RUBY @@ -129,6 +132,12 @@ def build_request_body(params) RUBY end + def format_path(path) + path.gsub(':version', 'v4') # Replace :version with v4 + .gsub(/\(\.\:format\)/, '') # Remove (.:format) + .gsub(/:(\w+)/, '{\1}') # Replace :param with {param} +end + def format_params(route) params = [] diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index e6e1257ad4504b..afe5190b49652e 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -1,12 +1,18 @@ # frozen_string_literal: true # rubocop:disable all -require 'openapi_helper' + +require 'swagger_helper' RSpec.describe "Events API", type: :request do + let(:user) { create(:user) } + + before do + sign_in user + end - path "/events(.:format)" do + path "/api/v4/events" do get 'List currently authenticated users eventss events' do - tags + tags ["events"] produces 'application/json' parameter name: "scope", in: :query, type: :string, description: 'Include all events across a user’s projects', example: "all", required: false parameter name: "page", in: :query, type: :integer, description: 'Current page number', default: 1, example: 1, required: false @@ -29,7 +35,7 @@ end - path "/users/:id/events(.:format)" do + path "/api/v4/users/{id}/events" do get 'Get the contribution events of a specified user' do tags ["events"] produces 'application/json' @@ -44,10 +50,12 @@ response '200', 'successful' do schema type: :object + let(:id) { user.id } run_test! end response '404', 'not found' do + let(:id) {'invlaid'} run_test! end end diff --git a/spec/support/known_rspec_metadata_keys.yml b/spec/support/known_rspec_metadata_keys.yml index 07d5313e2337ce..5e1c1c72f8b615 100644 --- a/spec/support/known_rspec_metadata_keys.yml +++ b/spec/support/known_rspec_metadata_keys.yml @@ -169,3 +169,7 @@ - :zoekt - :zoekt_cache_disabled - :zoekt_settings_enabled +- :path_item +- :operation +- :response +- :rswag diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 14c1214976e532..3866bcfb811f63 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require 'spec_helper' RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated @@ -24,12 +24,7 @@ paths: {}, servers: [ { - url: 'https://{defaultHost}', - variables: { - defaultHost: { - default: 'www.example.com' - } - } + url: 'http://gdk.test:3000' } ] } diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml new file mode 100644 index 00000000000000..416e34769bbf22 --- /dev/null +++ b/swagger/v1/swagger.yaml @@ -0,0 +1,179 @@ +--- +openapi: 3.0.1 +info: + title: API V1 + version: v1 +paths: + "/api/v4/users/{id}/events": + get: + summary: Get the contribution events of a specified user + tags: + - - events + parameters: + - name: id + in: path + description: The ID or username of the user + required: true + schema: + type: string + - name: page + in: query + description: Current page number + default: 1 + example: 1 + required: false + schema: + type: integer + - name: per_page + in: query + description: Number of items per page + default: 20 + example: 20 + required: false + schema: + type: integer + - name: action + in: query + description: Event action to filter on + required: false + schema: + type: string + - name: target_type + in: query + description: "Event target type to filter on:\n * `issue` \n * `milestone` + \n * `merge_request` \n * `note` \n * `project` \n * `snippet` \n * `user` + \n * `wiki` \n * `design` \n " + enum: + - issue + - milestone + - merge_request + - note + - project + - snippet + - user + - wiki + - design + required: false + schema: + type: string + - name: before + in: query + format: date + description: Include only events created before this date + required: false + schema: + type: string + - name: after + in: query + format: date + description: Include only events created after this date + required: false + schema: + type: string + - name: sort + in: query + description: Return events sorted in ascending and descending order + default: desc + enum: + - asc + - desc + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + '404': + description: not found + "/api/v4/events": + get: + summary: List currently authenticated users eventss events + tags: + - - events + parameters: + - name: scope + in: query + description: Include all events across a user’s projects + example: all + required: false + schema: + type: string + - name: page + in: query + description: Current page number + default: 1 + example: 1 + required: false + schema: + type: integer + - name: per_page + in: query + description: Number of items per page + default: 20 + example: 20 + required: false + schema: + type: integer + - name: action + in: query + description: Event action to filter on + required: false + schema: + type: string + - name: target_type + in: query + description: "Event target type to filter on:\n * `issue` \n * `milestone` + \n * `merge_request` \n * `note` \n * `project` \n * `snippet` \n * `user` + \n * `wiki` \n * `design` \n " + enum: + - issue + - milestone + - merge_request + - note + - project + - snippet + - user + - wiki + - design + required: false + schema: + type: string + - name: before + in: query + format: date + description: Include only events created before this date + required: false + schema: + type: string + - name: after + in: query + format: date + description: Include only events created after this date + required: false + schema: + type: string + - name: sort + in: query + description: Return events sorted in ascending and descending order + default: desc + enum: + - asc + - desc + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + '404': + description: not found +servers: +- url: http://gdk.test:3000 -- GitLab From 0bc5d8560cb0f53fe1c79befb3faead9f59bcec6 Mon Sep 17 00:00:00 2001 From: Vlad Wolanyk Date: Wed, 10 Sep 2025 17:05:01 -0400 Subject: [PATCH 3/6] Add response schema Parse response entity for docs --- lib/grape_to_rswag.rb | 167 ++++++++++++++++++++++++++++++++--- spec/requests/events_spec.rb | 57 ++++++++---- swagger/v1/swagger.yaml | 91 +++++++++++++++++-- 3 files changed, 280 insertions(+), 35 deletions(-) diff --git a/lib/grape_to_rswag.rb b/lib/grape_to_rswag.rb index 0c57627f17cc27..f9441839889463 100644 --- a/lib/grape_to_rswag.rb +++ b/lib/grape_to_rswag.rb @@ -96,18 +96,10 @@ def build_route_test(route) operation << build_request_body(body_params) unless body_params.empty? end - # Add responses + # Theese are the the test blocks based on possible responses operation << "" - operation << " response '200', 'successful' do" - operation << " schema type: :object" - operation << " run_test!" + operation << build_responses(route) operation << " end" - operation << "" - operation << " response '404', 'not found' do" - operation << " run_test!" - operation << " end" - - operation << " end" operation.join("\n") end @@ -277,6 +269,161 @@ def grape_type_to_openapi(type) type_mapping[type.to_s] || { type: :string } end + def build_responses(route) + responses = [] + + # Build success response (200) + responses << build_success_response(route) + + # Build failure responses from http_codes(defined in API class definition as failure) + if route.http_codes && !route.http_codes.empty? + route.http_codes.each do |http_code| + responses << build_failure_response(http_code[:code], http_code[:message]) + end + else + # Default failure response if no http_codes specified + responses << build_failure_response(404, 'not found') + end + + responses.join("\n") + end + + def build_success_response(route) + # Check if there's an entity first + if route.respond_to?(:entity) && route.entity + # Determine if response should be an array + is_array = route.attributes.is_array + + response_lines = [] + response_lines << " response '200', 'successful' do" + + # Build schema with entity properties + schema_content = build_response_schema(route, is_array) + response_lines << schema_content + + response_lines << " run_test!" + response_lines << " end" + + response_lines.join("\n") + else + # No entity - return 204 No Content + response_lines = [] + response_lines << " response '204', 'no content' do" + response_lines << " run_test!" + response_lines << " end" + + response_lines.join("\n") + end + end + + def build_response_schema(route, is_array) + # Check if route has an entity + if route.respond_to?(:entity) && route.entity + properties = extract_entity_properties(route.entity) + + if is_array + build_array_schema_with_properties(properties) + else + build_object_schema_with_properties(properties) + end + else + # No entity - check if this is a destroy action or similar + method = route.attributes.request_method.downcase + if method == 'delete' || route.description&.match?(/destroy|delete/i) + # Destroy actions typically return empty objects or 204 status + " schema type: :object" + elsif is_array + # Default array response without properties + " schema type: :array, items: { type: :object }" + else + # Default object response without properties + " schema type: :object" + end + end + end + + def extract_entity_properties(entity_class) + properties = {} + + # Get documentation directly from the entity + entity_class.documentation.each do |field_name, field_info| + property_name = field_name.to_s + + # Convert grape type to OpenAPI type using existing method + type_info = grape_type_to_openapi(field_info[:type] || field_info['type']) + + property_info = type_info.dup + + # Add example if available + example = field_info[:example] || field_info['example'] + property_info[:example] = example if example + + # Add description if available + desc = field_info[:desc] || field_info['desc'] || field_info[:description] || field_info['description'] + property_info[:description] = desc if desc + + properties[property_name] = property_info + end + + properties + end + + def build_object_schema_with_properties(properties) + if properties.empty? + return " schema type: :object" + end + + schema_lines = [] + schema_lines << " schema type: :object, properties: {" + + properties.each_with_index do |(name, info), index| + line = " #{name}: { type: :#{info[:type]}" + line += ", description: '#{info[:description].gsub("'", "\\'")}'" if info[:description] + line += ", example: #{info[:example].inspect}" if info[:example] + line += " }" + line += "," unless index == properties.length - 1 + schema_lines << line + end + + schema_lines << " }" + schema_lines.join("\n") + end + + def build_array_schema_with_properties(properties) + if properties.empty? + return " schema type: :array, items: { type: :object }" + end + + schema_lines = [] + schema_lines << " schema type: :array, items: {" + schema_lines << " type: :object," + schema_lines << " properties: {" + + properties.each_with_index do |(name, info), index| + line = " #{name}: { type: :#{info[:type]}" + line += ", description: '#{info[:description].gsub("'", "\\'")}'" if info[:description] + line += ", example: #{info[:example].inspect}" if info[:example] + line += " }" + line += "," unless index == properties.length - 1 + schema_lines << line + end + + schema_lines << " }" + schema_lines << " }" + schema_lines.join("\n") + end + + + def build_failure_response(code, message) + response_lines = [] + response_lines << " response '#{code}', '#{message.downcase}' do" + response_lines << " run_test!" + response_lines << " end" + + response_lines.join("\n") + end + + def write_file(resource, content) FileUtils.mkdir_p(@output_dir) file_path = File.join(@output_dir, "#{resource}_spec.rb") diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index afe5190b49652e..9e8b5926965119 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -4,15 +4,10 @@ require 'swagger_helper' RSpec.describe "Events API", type: :request do - let(:user) { create(:user) } - before do - sign_in user - end - - path "/api/v4/events" do + path "/events" do get 'List currently authenticated users eventss events' do - tags ["events"] + tags produces 'application/json' parameter name: "scope", in: :query, type: :string, description: 'Include all events across a user’s projects', example: "all", required: false parameter name: "page", in: :query, type: :integer, description: 'Current page number', default: 1, example: 1, required: false @@ -24,18 +19,33 @@ parameter name: "sort", in: :query, type: :string, description: 'Return events sorted in ascending and descending order', default: "desc", enum: ["asc", "desc"], required: false response '200', 'successful' do - schema type: :object + schema type: :array, items: { + type: :object, + properties: { + id: { type: :string, example: 1 }, + project_id: { type: :string, example: 2 }, + action_name: { type: :string, example: "closed" }, + target_id: { type: :string, example: 160 }, + target_iid: { type: :string, example: 157 }, + target_type: { type: :string, example: "Issue" }, + author_id: { type: :string, example: 25 }, + target_title: { type: :string, example: "Public project search field" }, + created_at: { type: :string, example: "2017-02-09T10:43:19.667Z" }, + imported: { type: :string }, + imported_from: { type: :string, example: "none" }, + author_username: { type: :string, example: "root" } + } + } run_test! end - - response '404', 'not found' do + response '401', 'unauthorized' do run_test! end - end + end end - path "/api/v4/users/{id}/events" do + path "/users/{id}/events" do get 'Get the contribution events of a specified user' do tags ["events"] produces 'application/json' @@ -49,16 +59,29 @@ parameter name: "sort", in: :query, type: :string, description: 'Return events sorted in ascending and descending order', default: "desc", enum: ["asc", "desc"], required: false response '200', 'successful' do - schema type: :object - let(:id) { user.id } + schema type: :array, items: { + type: :object, + properties: { + id: { type: :string, example: 1 }, + project_id: { type: :string, example: 2 }, + action_name: { type: :string, example: "closed" }, + target_id: { type: :string, example: 160 }, + target_iid: { type: :string, example: 157 }, + target_type: { type: :string, example: "Issue" }, + author_id: { type: :string, example: 25 }, + target_title: { type: :string, example: "Public project search field" }, + created_at: { type: :string, example: "2017-02-09T10:43:19.667Z" }, + imported: { type: :string }, + imported_from: { type: :string, example: "none" }, + author_username: { type: :string, example: "root" } + } + } run_test! end - response '404', 'not found' do - let(:id) {'invlaid'} run_test! end - end + end end end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 416e34769bbf22..d545cb31a7f4aa 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -4,7 +4,7 @@ info: title: API V1 version: v1 paths: - "/api/v4/users/{id}/events": + "/users/{id}/events": get: summary: Get the contribution events of a specified user tags: @@ -86,14 +86,51 @@ paths: content: application/json: schema: - type: object + type: array + items: + type: object + properties: + id: + type: string + example: 1 + project_id: + type: string + example: 2 + action_name: + type: string + example: closed + target_id: + type: string + example: 160 + target_iid: + type: string + example: 157 + target_type: + type: string + example: Issue + author_id: + type: string + example: 25 + target_title: + type: string + example: Public project search field + created_at: + type: string + example: '2017-02-09T10:43:19.667Z' + imported: + type: string + imported_from: + type: string + example: none + author_username: + type: string + example: root '404': description: not found - "/api/v4/events": + "/events": get: summary: List currently authenticated users eventss events - tags: - - - events + tags: [] parameters: - name: scope in: query @@ -172,8 +209,46 @@ paths: content: application/json: schema: - type: object - '404': - description: not found + type: array + items: + type: object + properties: + id: + type: string + example: 1 + project_id: + type: string + example: 2 + action_name: + type: string + example: closed + target_id: + type: string + example: 160 + target_iid: + type: string + example: 157 + target_type: + type: string + example: Issue + author_id: + type: string + example: 25 + target_title: + type: string + example: Public project search field + created_at: + type: string + example: '2017-02-09T10:43:19.667Z' + imported: + type: string + imported_from: + type: string + example: none + author_username: + type: string + example: root + '401': + description: unauthorized servers: - url: http://gdk.test:3000 -- GitLab From 0aefa9e078dd21b3710815da39f0b1a486f61eca Mon Sep 17 00:00:00 2001 From: Vlad Wolanyk Date: Thu, 11 Sep 2025 16:46:42 -0400 Subject: [PATCH 4/6] Refactor comment clean up Clean up code, refactor add comprehensive comments --- lib/grape_to_rswag.rb | 204 ++++-- spec/requests/rswag/environments_spec.rb | 286 ++++++++ spec/requests/{ => rswag}/events_spec.rb | 36 +- swagger/v1/swagger.yaml | 842 ++++++++++++++++++++++- 4 files changed, 1263 insertions(+), 105 deletions(-) create mode 100644 spec/requests/rswag/environments_spec.rb rename spec/requests/{ => rswag}/events_spec.rb (76%) diff --git a/lib/grape_to_rswag.rb b/lib/grape_to_rswag.rb index f9441839889463..5ad424b3c5fff6 100644 --- a/lib/grape_to_rswag.rb +++ b/lib/grape_to_rswag.rb @@ -5,39 +5,44 @@ class GrapeToRswag attr_reader :api_class, :output_dir - def initialize(api_class, output_dir: 'spec/requests') + def initialize(api_class, output_dir: 'spec/requests/rswag') @api_class = api_class @output_dir = output_dir end def convert_to_spec + # Any methods not defined in this class are `Grape::DSL#methods` + # acting on the existing grape entitites routes = api_class.routes - # Generate the spec content + #the main funtionality content = build_spec_file(routes) - # Derive the resource name from the API class - # This will create a filename like "users_spec.rb" for UsersAPI - resource_name = api_class.name.demodulize.underscore.gsub(/_api$/, '') + # Build the file name from the class name + resource_name = api_class.name.demodulize.underscore # Write the spec to file and return the file path file_path = write_file(resource_name, content) puts "Spec written to: #{file_path}" - # Return both the content and the file path (or just the file path) + # prints out the content in the console for visual ref { content: content, file_path: file_path } end private + # *** BUILDER METHODS -- building spec sections *** + def build_spec_file(routes) - # Follows naming convention, may want to un-camelcase complex names - # this is the base of the spec + # base structure for the spec, we will need to add feature_category + # May want to format class name a bit better + # rubocop disabled only for development + # swagger helper may need to be configured more, autogenerated with gem <<~RUBY # frozen_string_literal: true - # rubocop:disable all + # rubocop:disable all -- for development require 'swagger_helper' @@ -48,8 +53,8 @@ def build_spec_file(routes) end def build_paths(routes) - # need this because there can be multiple routes under the same path and need to - # be organized this way for open api spec generation + # there can be multiple routes under the each path + # this method organizes routes within paths # the extra lines in the join is just for readability paths = routes.group_by(&:path) @@ -60,7 +65,9 @@ def build_paths(routes) end def build_path_block(path, routes) - # I think we will need to replace :version with v4 in path + # we need to format_path because + # - url params need to be wrapped in curly braces {} + # - url param `:version` in path even though we only use v4 <<~RUBY @@ -71,11 +78,13 @@ def build_path_block(path, routes) end def build_route_test(route) + # builds individual route tests route_attrs = route.attributes method = route_attrs.request_method.downcase - # takes care of apostrophes - description = route.description.gsub("'", "\\'") || "#{method.upcase} #{route.path}" - tags = route_attrs.tags + # formats descriptions or adds default one + description = route.description&.gsub("'", "\\'") || "#{method.upcase} #{route.path}" + # for instances where there are no tags adds one for the class name at the least + tags = route_attrs.tags || [ api_class.name.demodulize.underscore.to_s ] params = format_params(route) operation = [] @@ -96,17 +105,28 @@ def build_route_test(route) operation << build_request_body(body_params) unless body_params.empty? end - # Theese are the the test blocks based on possible responses + # These are the the test blocks based on possible responses operation << "" operation << build_responses(route) operation << " end" + # each array element represents a test line operation.join("\n") end def build_request_body(params) + # requests that create or update resources need a request body + properties = params.map do |name, details| - type = grape_type_to_openapi(details[:type]) - " #{name}: { type: :#{type} }" + type_info = grape_type_to_openapi(details[:type]) + + if type_info[:oneOf] + # Handle oneOf case + one_of_schemas = type_info[:oneOf].map(&:inspect).join(', ') + " #{name}: { oneOf: [#{one_of_schemas}] }" + else + # Handle simple type case + " #{name}: { type: #{type_info[:type].inspect} }" + end end.join(",\n") required = params.select { |_, v| v[:required] }.keys @@ -124,27 +144,34 @@ def build_request_body(params) RUBY end + # *** FORMATTING METHODS -- format grape dsl formatting to rswag formatting *** + + # referenced above, conform to rswag dsl def format_path(path) path.gsub(':version', 'v4') # Replace :version with v4 .gsub(/\(\.\:format\)/, '') # Remove (.:format) .gsub(/:(\w+)/, '{\1}') # Replace :param with {param} -end + end def format_params(route) params = [] + # just in case return (this may not be needed) + return params unless route.params - # Path parameters + # Takes care of path parameters path_params = route.path.scan(/:(\w+)/).flatten - %w[version format] - path_params.each do |param| + path_params.each do |param_name| param_hash = { - name: param, + name: param_name, in: :path, required: true } + # param details include type, documentation, requirement boolean, desc, enums, defaults + details = route.params[param_name] || route.params[param_name.to_sym] + # Check if this path param has details in route.params (check both string and symbol keys) - if route.params && (route.params[param] || route.params[param.to_sym]) - details = route.params[param] || route.params[param.to_sym] + unless details.blank? # Get type from details type_info = grape_type_to_openapi(details[:type]) @@ -162,49 +189,49 @@ def format_params(route) param_hash[:enum] = details[:values] if details[:values] else # Default type if no details found - param_hash[:type] = param == 'id' ? :integer : :string + param_hash[:type] = param_name == 'id' ? :integer : :string # Default description for common params (only if no details were found) - param_hash[:description] = 'The ID of the resource' if param == 'id' + param_hash[:description] = 'The ID of the resource' if param_name == 'id' end params << param_hash end - # Query parameters (for GET requests) - if route.request_method.downcase == 'get' && route.params - route.params.each do |name, details| - # skip if already added as path param - next if params.any? { |p| p[:name] == name.to_s } + # select and format query params ==> in: :query + query_params = route.params.reject { |name, _| path_params.include?(name.to_s) } + query_params.each do |name, details| - param_hash = { - name: name.to_s, - in: :query - } + param_hash = { + name: name.to_s, + in: :query + } - # Merge the type information properly - type_info = grape_type_to_openapi(details[:type]) - param_hash.merge!(type_info) + # Merge the type information properly + type_info = grape_type_to_openapi(details[:type]) + param_hash.merge!(type_info) - # Add description from Grape params - param_hash[:description] = details[:desc] if details[:desc] + # Add description from Grape params + param_hash[:description] = details[:desc] if details[:desc] - # Add default value if present - param_hash[:default] = details[:default] if details[:default] + # Add default value if present + # There is an unhandled issue here where we do have some dynamic defaults defined + # as procs ie in environment there is a default: -> { 30.days.ago } + # for this POC choosing to ignore this edge case + param_hash[:default] = details[:default] if details[:default] && !details[:default].is_a?(Proc) - # Add enum values if present - param_hash[:enum] = details[:values] if details[:values] + # Add enum values if present + param_hash[:enum] = details[:values] if details[:values] - # Add example from documentation if present - if details[:documentation] && details[:documentation][:example] - param_hash[:example] = details[:documentation][:example] - end + # Add example from documentation if present + if details[:documentation] && details[:documentation][:example] + param_hash[:example] = details[:documentation][:example] + end - # Add required field - param_hash[:required] = details[:required] || false + # Add required field + param_hash[:required] = details[:required] || false - params << param_hash - end + params << param_hash end params @@ -212,11 +239,21 @@ def format_params(route) def format_parameter_line(param) # Build parameter line with proper formatting + # I have attempted to cover all the different param options + # it is possible there are some I have missed + # calls param_value#inspect to ensure proper syntax, escape single quotes and apostrophes etc... + param_options = [] param_options << "name: #{param[:name].inspect}" param_options << "in: #{param[:in].inspect}" - param_options << "type: #{param[:type].inspect}" if param[:type] + # Handle oneOf vs simple types + if param[:oneOf] + one_of_schemas = param[:oneOf].map(&:inspect).join(', ') + param_options << "schema: { oneOf: [#{one_of_schemas}] }" + else + param_options << "type: #{param[:type].inspect}" if param[:type] + end param_options << "format: #{param[:format].inspect}" if param[:format] @@ -228,11 +265,7 @@ def format_parameter_line(param) end # Add description if present - if param[:description] - # Escape single quotes in description - escaped_desc = param[:description].gsub("'", "\\'") - param_options << "description: '#{escaped_desc}'" - end + param_options << "description: #{param[:description].inspect}" if param[:description] # Add default value if present param_options << "default: #{param[:default].inspect}" if param.key?(:default) @@ -252,6 +285,8 @@ def format_parameter_line(param) def grape_type_to_openapi(type) return { type: :string } unless type + # This may need to be expanded and likely defined in a constant + # likely we would extract param formatting into it's own class as well type_mapping = { 'String' => { type: :string }, 'Integer' => { type: :integer }, @@ -266,16 +301,27 @@ def grape_type_to_openapi(type) 'Hash' => { type: :object } } - type_mapping[type.to_s] || { type: :string } + # we need to handle the oneOf for param types + # there are many examples of the url param or an id param being passed an array of types + if type.match(/^\[(.+)\]$/) + # when an array passed to type is looks something like "[String, Integer]" + types = type.tr("[]", "").split(",").map(&:strip) + { oneOf: types.map { |t| type_mapping[t] || { type: :string } } } + else + type_mapping[type.to_s] || { type: :string } + end end + # *** RESPONSE BUILDERS - builds response bodies *** + + def build_responses(route) responses = [] # Build success response (200) responses << build_success_response(route) - # Build failure responses from http_codes(defined in API class definition as failure) + # Build failure responses from grapeMethod#http_codes(defined in API class definition as failure) if route.http_codes && !route.http_codes.empty? route.http_codes.each do |http_code| responses << build_failure_response(http_code[:code], http_code[:message]) @@ -291,7 +337,7 @@ def build_responses(route) def build_success_response(route) # Check if there's an entity first if route.respond_to?(:entity) && route.entity - # Determine if response should be an array + # Determine if response should be an array (this is an existing grape attr) is_array = route.attributes.is_array response_lines = [] @@ -307,6 +353,9 @@ def build_success_response(route) response_lines.join("\n") else # No entity - return 204 No Content + # I noticed that several of our destroy routes return response objects/entities + # if we do not want this and we want to always return 204 even if we previously + # defined a response, we could just code all delete actions to return 204 response_lines = [] response_lines << " response '204', 'no content' do" response_lines << " run_test!" @@ -342,6 +391,8 @@ def build_response_schema(route, is_array) end end + # Translates grape entities for responses + # builds/translates grapeEntity --> rswag response object for use with response schema def extract_entity_properties(entity_class) properties = {} @@ -349,7 +400,7 @@ def extract_entity_properties(entity_class) entity_class.documentation.each do |field_name, field_info| property_name = field_name.to_s - # Convert grape type to OpenAPI type using existing method + # Convert grape type to OpenAPI type using same method used for formatting params type_info = grape_type_to_openapi(field_info[:type] || field_info['type']) property_info = type_info.dup @@ -377,10 +428,15 @@ def build_object_schema_with_properties(properties) schema_lines << " schema type: :object, properties: {" properties.each_with_index do |(name, info), index| - line = " #{name}: { type: :#{info[:type]}" - line += ", description: '#{info[:description].gsub("'", "\\'")}'" if info[:description] - line += ", example: #{info[:example].inspect}" if info[:example] - line += " }" + if info[:oneOf] + one_of_schemas = info[:oneOf].map(&:inspect).join(', ') + line = " #{name}: { oneOf: [#{one_of_schemas}] }" + else + line = " #{name}: { type: :#{info[:type]}" + line += ", description: '#{info[:description].gsub("'", "\\'")}'" if info[:description] + line += ", example: #{info[:example].inspect}" if info[:example] + line += " }" + end line += "," unless index == properties.length - 1 schema_lines << line end @@ -400,10 +456,15 @@ def build_array_schema_with_properties(properties) schema_lines << " properties: {" properties.each_with_index do |(name, info), index| - line = " #{name}: { type: :#{info[:type]}" - line += ", description: '#{info[:description].gsub("'", "\\'")}'" if info[:description] - line += ", example: #{info[:example].inspect}" if info[:example] - line += " }" + if info[:oneOf] + one_of_schemas = info[:oneOf].map(&:inspect).join(', ') + line = " #{name}: { oneOf: [#{one_of_schemas}] }" + else + line = " #{name}: { type: :#{info[:type]}" + line += ", description: '#{info[:description].gsub("'", "\\'")}'" if info[:description] + line += ", example: #{info[:example].inspect}" if info[:example] + line += " }" + end line += "," unless index == properties.length - 1 schema_lines << line end @@ -414,6 +475,7 @@ def build_array_schema_with_properties(properties) end + def build_failure_response(code, message) response_lines = [] response_lines << " response '#{code}', '#{message.downcase}' do" @@ -423,7 +485,7 @@ def build_failure_response(code, message) response_lines.join("\n") end - +# *** FILE WRITER def write_file(resource, content) FileUtils.mkdir_p(@output_dir) file_path = File.join(@output_dir, "#{resource}_spec.rb") diff --git a/spec/requests/rswag/environments_spec.rb b/spec/requests/rswag/environments_spec.rb new file mode 100644 index 00000000000000..d581c304e02bf4 --- /dev/null +++ b/spec/requests/rswag/environments_spec.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true +# rubocop:disable all -- for development + +require 'swagger_helper' + +RSpec.describe "Environments API", type: :request do + + path "/api/v4/projects/{id}/environments" do + get 'List environments' do + tags ["environments"] + produces 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "page", in: :query, type: :integer, description: "Current page number", default: 1, example: 1, required: false + parameter name: "per_page", in: :query, type: :integer, description: "Number of items per page", default: 20, example: 20, required: false + parameter name: "name", in: :query, type: :string, description: "Return the environment with this name. Mutually exclusive with search", required: false + parameter name: "search", in: :query, type: :string, description: "Return list of environments matching the search criteria. Mutually exclusive with name. Must be at least 3 characters.", required: false + parameter name: "states", in: :query, type: :string, description: "List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments", enum: ["stopped", "stopping", "available"], required: false + + response '200', 'successful' do + schema type: :array, items: { + type: :object, + properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + tier: { type: :string, example: "development" }, + state: { type: :string, example: "available" }, + auto_stop_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + description: { type: :string, example: "description" }, + auto_stop_setting: { type: :string, example: "always" } + } + } + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + end + post 'Create a new environment' do + tags ["environments"] + produces 'application/json' + consumes 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "name", in: :query, type: :string, description: "The name of the environment", required: true + parameter name: "external_url", in: :query, type: :string, description: "Place to link to for this environment", required: false + parameter name: "slug", in: :query, type: :string, required: false + parameter name: "tier", in: :query, type: :string, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", enum: ["production", "staging", "testing", "development", "other"], required: false + parameter name: "cluster_agent_id", in: :query, type: :integer, description: "The ID of the Cluster Agent to associate with this environment", required: false + parameter name: "kubernetes_namespace", in: :query, type: :string, description: "The Kubernetes namespace to associate with this environment", required: false + parameter name: "flux_resource_path", in: :query, type: :string, description: "The Flux resource path to associate with this environment", required: false + parameter name: "description", in: :query, type: :string, description: "The description of the environment", required: false + parameter name: "auto_stop_setting", in: :query, type: :string, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", enum: ["always", "with_action"], required: false + + response '200', 'successful' do + schema type: :object, properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + tier: { type: :string, example: "development" }, + state: { type: :string, example: "available" }, + auto_stop_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + description: { type: :string, example: "description" }, + auto_stop_setting: { type: :string, example: "always" } + } + run_test! + end + response '400', 'bad request' do + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + end + end + + + path "/api/v4/projects/{id}/environments/{environment_id}" do + put 'Update an existing environment' do + tags ["environments"] + produces 'application/json' + consumes 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true + parameter name: "external_url", in: :query, type: :string, description: "The new URL on which this deployment is viewable", required: false + parameter name: "slug", in: :query, type: :string, required: false + parameter name: "tier", in: :query, type: :string, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", enum: ["production", "staging", "testing", "development", "other"], required: false + parameter name: "cluster_agent_id", in: :query, type: :integer, description: "The ID of the Cluster Agent to associate with this environment", required: false + parameter name: "kubernetes_namespace", in: :query, type: :string, description: "The Kubernetes namespace to associate with this environment", required: false + parameter name: "flux_resource_path", in: :query, type: :string, description: "The Flux resource path to associate with this environment", required: false + parameter name: "description", in: :query, type: :string, description: "The description of the environment", required: false + parameter name: "auto_stop_setting", in: :query, type: :string, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", enum: ["always", "with_action"], required: false + + response '200', 'successful' do + schema type: :object, properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + tier: { type: :string, example: "development" }, + state: { type: :string, example: "available" }, + auto_stop_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + description: { type: :string, example: "description" }, + auto_stop_setting: { type: :string, example: "always" } + } + run_test! + end + response '400', 'bad request' do + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + end + delete 'Delete an environment' do + tags ["environments"] + produces 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true + + response '200', 'successful' do + schema type: :object, properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + tier: { type: :string, example: "development" }, + state: { type: :string, example: "available" }, + auto_stop_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + description: { type: :string, example: "description" }, + auto_stop_setting: { type: :string, example: "always" } + } + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + end + get 'Get a specific environment' do + tags ["environments"] + produces 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true + + response '200', 'successful' do + schema type: :object, properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + tier: { type: :string, example: "development" }, + state: { type: :string, example: "available" }, + auto_stop_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + description: { type: :string, example: "description" }, + auto_stop_setting: { type: :string, example: "always" } + } + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + end + end + + + path "/api/v4/projects/{id}/environments/review_apps" do + delete 'Delete multiple stopped review apps' do + tags ["environments"] + produces 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "before", in: :query, type: :string, format: "date-time", description: "The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)", required: false + parameter name: "limit", in: :query, type: :integer, description: "Maximum number of environments to delete. Defaults to 100", default: 100, enum: 1..1000, required: false + parameter name: "dry_run", in: :query, type: :string, description: "Defaults to true for safety reasons. It performs a dry run where no actual deletion will be performed. Set to false to actually delete the environment", default: true, required: false + + response '200', 'successful' do + schema type: :object, properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" } + } + run_test! + end + response '400', 'bad request' do + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + response '409', 'conflict' do + run_test! + end + end + end + + + path "/api/v4/projects/{id}/environments/{environment_id}/stop" do + post 'Stop an environment' do + tags ["environments"] + produces 'application/json' + consumes 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true + parameter name: "force", in: :query, type: :string, description: "Force environment to stop without executing `on_stop` actions", required: false + + response '200', 'successful' do + schema type: :object, properties: { + id: { type: :string, example: 1 }, + name: { type: :string, example: "deploy" }, + slug: { type: :string, example: "deploy" }, + external_url: { type: :string, example: "https://deploy.gitlab.example.com" }, + created_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + updated_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + tier: { type: :string, example: "development" }, + state: { type: :string, example: "available" }, + auto_stop_at: { type: :string, example: "2019-05-25T18:55:13.252Z" }, + description: { type: :string, example: "description" }, + auto_stop_setting: { type: :string, example: "always" } + } + run_test! + end + response '400', 'bad request' do + run_test! + end + response '401', 'unauthorized' do + run_test! + end + response '404', 'not found' do + run_test! + end + end + end + + + path "/api/v4/projects/{id}/environments/stop_stale" do + post 'Stop stale environments' do + tags ["environments"] + produces 'application/json' + consumes 'application/json' + parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true + parameter name: "before", in: :query, type: :string, format: "date-time", description: "Stop all environments that were last modified or deployed to before this date.", required: true + + response '204', 'no content' do + run_test! + end + response '400', 'bad request' do + run_test! + end + response '401', 'unauthorized' do + run_test! + end + end + end + +end diff --git a/spec/requests/events_spec.rb b/spec/requests/rswag/events_spec.rb similarity index 76% rename from spec/requests/events_spec.rb rename to spec/requests/rswag/events_spec.rb index 9e8b5926965119..b8c64732c8ad91 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/rswag/events_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# rubocop:disable all +# rubocop:disable all -- for development require 'swagger_helper' @@ -7,16 +7,16 @@ path "/events" do get 'List currently authenticated users eventss events' do - tags + tags ["events"] produces 'application/json' - parameter name: "scope", in: :query, type: :string, description: 'Include all events across a user’s projects', example: "all", required: false - parameter name: "page", in: :query, type: :integer, description: 'Current page number', default: 1, example: 1, required: false - parameter name: "per_page", in: :query, type: :integer, description: 'Number of items per page', default: 20, example: 20, required: false - parameter name: "action", in: :query, type: :string, description: 'Event action to filter on', required: false - parameter name: "target_type", in: :query, type: :string, description: 'Event target type to filter on', enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false - parameter name: "before", in: :query, type: :string, format: :date, description: 'Include only events created before this date', required: false - parameter name: "after", in: :query, type: :string, format: :date, description: 'Include only events created after this date', required: false - parameter name: "sort", in: :query, type: :string, description: 'Return events sorted in ascending and descending order', default: "desc", enum: ["asc", "desc"], required: false + parameter name: "scope", in: :query, type: :string, description: "Include all events across a user’s projects", example: "all", required: false + parameter name: "page", in: :query, type: :integer, description: "Current page number", default: 1, example: 1, required: false + parameter name: "per_page", in: :query, type: :integer, description: "Number of items per page", default: 20, example: 20, required: false + parameter name: "action", in: :query, type: :string, description: "Event action to filter on", required: false + parameter name: "target_type", in: :query, type: :string, description: "Event target type to filter on", enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false + parameter name: "before", in: :query, type: :string, format: :date, description: "Include only events created before this date", required: false + parameter name: "after", in: :query, type: :string, format: :date, description: "Include only events created after this date", required: false + parameter name: "sort", in: :query, type: :string, description: "Return events sorted in ascending and descending order", default: "desc", enum: ["asc", "desc"], required: false response '200', 'successful' do schema type: :array, items: { @@ -49,14 +49,14 @@ get 'Get the contribution events of a specified user' do tags ["events"] produces 'application/json' - parameter name: "id", in: :path, type: :string, description: 'The ID or username of the user', required: true - parameter name: "page", in: :query, type: :integer, description: 'Current page number', default: 1, example: 1, required: false - parameter name: "per_page", in: :query, type: :integer, description: 'Number of items per page', default: 20, example: 20, required: false - parameter name: "action", in: :query, type: :string, description: 'Event action to filter on', required: false - parameter name: "target_type", in: :query, type: :string, description: 'Event target type to filter on', enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false - parameter name: "before", in: :query, type: :string, format: :date, description: 'Include only events created before this date', required: false - parameter name: "after", in: :query, type: :string, format: :date, description: 'Include only events created after this date', required: false - parameter name: "sort", in: :query, type: :string, description: 'Return events sorted in ascending and descending order', default: "desc", enum: ["asc", "desc"], required: false + parameter name: "id", in: :path, type: :string, description: "The ID or username of the user", required: true + parameter name: "page", in: :query, type: :integer, description: "Current page number", default: 1, example: 1, required: false + parameter name: "per_page", in: :query, type: :integer, description: "Number of items per page", default: 20, example: 20, required: false + parameter name: "action", in: :query, type: :string, description: "Event action to filter on", required: false + parameter name: "target_type", in: :query, type: :string, description: "Event target type to filter on", enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false + parameter name: "before", in: :query, type: :string, format: :date, description: "Include only events created before this date", required: false + parameter name: "after", in: :query, type: :string, format: :date, description: "Include only events created after this date", required: false + parameter name: "sort", in: :query, type: :string, description: "Return events sorted in ascending and descending order", default: "desc", enum: ["asc", "desc"], required: false response '200', 'successful' do schema type: :array, items: { diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index d545cb31a7f4aa..91496f9c6581e6 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -4,18 +4,828 @@ info: title: API V1 version: v1 paths: - "/users/{id}/events": + "/api/v4/projects/{id}/environments/{environment_id}/stop": + post: + summary: Stop an environment + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: environment_id + in: path + description: The ID of the environment + required: true + schema: + type: integer + - name: force + in: query + description: Force environment to stop without executing `on_stop` actions + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '400': + description: bad request + '401': + description: unauthorized + '404': + description: not found + "/api/v4/projects/{id}/environments": get: - summary: Get the contribution events of a specified user + summary: List environments tags: - - - events + - - environments parameters: - name: id in: path - description: The ID or username of the user + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: page + in: query + description: Current page number + default: 1 + example: 1 + required: false + schema: + type: integer + - name: per_page + in: query + description: Number of items per page + default: 20 + example: 20 + required: false + schema: + type: integer + - name: name + in: query + description: Return the environment with this name. Mutually exclusive with + search + required: false + schema: + type: string + - name: search + in: query + description: Return list of environments matching the search criteria. Mutually + exclusive with name. Must be at least 3 characters. + required: false + schema: + type: string + - name: states + in: query + description: "List all environments that match a specific state. Accepted + values: `available`, `stopping`, or `stopped`. If no state value given, + returns all environments:\n * `stopped` \n * `stopping` \n * `available` + \n " + enum: + - stopped + - stopping + - available + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '401': + description: unauthorized + '404': + description: not found + post: + summary: Create a new environment + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: name + in: query + description: The name of the environment + required: true + schema: + type: string + - name: external_url + in: query + description: Place to link to for this environment + required: false + schema: + type: string + - name: slug + in: query + required: false + schema: + type: string + - name: tier + in: query + description: "The tier of the new environment. Allowed values are `production`, + `staging`, `testing`, `development`, and `other`:\n * `production` \n * + `staging` \n * `testing` \n * `development` \n * `other` \n " + enum: + - production + - staging + - testing + - development + - other + required: false + schema: + type: string + - name: cluster_agent_id + in: query + description: The ID of the Cluster Agent to associate with this environment + required: false + schema: + type: integer + - name: kubernetes_namespace + in: query + description: The Kubernetes namespace to associate with this environment + required: false + schema: + type: string + - name: flux_resource_path + in: query + description: The Flux resource path to associate with this environment + required: false + schema: + type: string + - name: description + in: query + description: The description of the environment + required: false + schema: + type: string + - name: auto_stop_setting + in: query + description: The auto stop setting for the environment. Allowed values are + `always` and `with_action` + enum: + - always + - with_action + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '400': + description: bad request + '401': + description: unauthorized + '404': + description: not found + "/api/v4/projects/{id}/environments/{environment_id}": + put: + summary: Update an existing environment + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: environment_id + in: path + description: The ID of the environment + required: true + schema: + type: integer + - name: external_url + in: query + description: The new URL on which this deployment is viewable + required: false + schema: + type: string + - name: slug + in: query + required: false + schema: + type: string + - name: tier + in: query + description: "The tier of the new environment. Allowed values are `production`, + `staging`, `testing`, `development`, and `other`:\n * `production` \n * + `staging` \n * `testing` \n * `development` \n * `other` \n " + enum: + - production + - staging + - testing + - development + - other + required: false + schema: + type: string + - name: cluster_agent_id + in: query + description: The ID of the Cluster Agent to associate with this environment + required: false + schema: + type: integer + - name: kubernetes_namespace + in: query + description: The Kubernetes namespace to associate with this environment + required: false + schema: + type: string + - name: flux_resource_path + in: query + description: The Flux resource path to associate with this environment + required: false + schema: + type: string + - name: description + in: query + description: The description of the environment + required: false + schema: + type: string + - name: auto_stop_setting + in: query + description: The auto stop setting for the environment. Allowed values are + `always` and `with_action` + enum: + - always + - with_action + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '400': + description: bad request + '401': + description: unauthorized + '404': + description: not found + delete: + summary: Delete an environment + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: environment_id + in: path + description: The ID of the environment + required: true + schema: + type: integer + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '401': + description: unauthorized + '404': + description: not found + get: + summary: Get a specific environment + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: environment_id + in: path + description: The ID of the environment + required: true + schema: + type: integer + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '401': + description: unauthorized + '404': + description: not found + "/api/v4/projects/{id}/environments/review_apps": + delete: + summary: Delete multiple stopped review apps + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: before + in: query + format: date-time + description: The date before which environments can be deleted. Defaults to + 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) + required: false + schema: + type: string + - name: limit + in: query + description: "Maximum number of environments to delete. Defaults to 100:\n + * `1` \n * `2` \n * `3` \n * `4` \n * `5` \n * `6` \n * `7` \n * `8` \n + * `9` \n * `10` \n * `11` \n * `12` \n * `13` \n * `14` \n * `15` \n * `16` + \n * `17` \n * `18` \n * `19` \n * `20` \n * `21` \n * `22` \n * `23` \n + * `24` \n * `25` \n * `26` \n * `27` \n * `28` \n * `29` \n * `30` \n * + `31` \n * `32` \n * `33` \n * `34` \n * `35` \n * `36` \n * `37` \n * `38` + \n * `39` \n * `40` \n * `41` \n * `42` \n * `43` \n * `44` \n * `45` \n + * `46` \n * `47` \n * `48` \n * `49` \n * `50` \n * `51` \n * `52` \n * + `53` \n * `54` \n * `55` \n * `56` \n * `57` \n * `58` \n * `59` \n * `60` + \n * `61` \n * `62` \n * `63` \n * `64` \n * `65` \n * `66` \n * `67` \n + * `68` \n * `69` \n * `70` \n * `71` \n * `72` \n * `73` \n * `74` \n * + `75` \n * `76` \n * `77` \n * `78` \n * `79` \n * `80` \n * `81` \n * `82` + \n * `83` \n * `84` \n * `85` \n * `86` \n * `87` \n * `88` \n * `89` \n + * `90` \n * `91` \n * `92` \n * `93` \n * `94` \n * `95` \n * `96` \n * + `97` \n * `98` \n * `99` \n * `100` \n * `101` \n * `102` \n * `103` \n + * `104` \n * `105` \n * `106` \n * `107` \n * `108` \n * `109` \n * `110` + \n * `111` \n * `112` \n * `113` \n * `114` \n * `115` \n * `116` \n * `117` + \n * `118` \n * `119` \n * `120` \n * `121` \n * `122` \n * `123` \n * `124` + \n * `125` \n * `126` \n * `127` \n * `128` \n * `129` \n * `130` \n * `131` + \n * `132` \n * `133` \n * `134` \n * `135` \n * `136` \n * `137` \n * `138` + \n * `139` \n * `140` \n * `141` \n * `142` \n * `143` \n * `144` \n * `145` + \n * `146` \n * `147` \n * `148` \n * `149` \n * `150` \n * `151` \n * `152` + \n * `153` \n * `154` \n * `155` \n * `156` \n * `157` \n * `158` \n * `159` + \n * `160` \n * `161` \n * `162` \n * `163` \n * `164` \n * `165` \n * `166` + \n * `167` \n * `168` \n * `169` \n * `170` \n * `171` \n * `172` \n * `173` + \n * `174` \n * `175` \n * `176` \n * `177` \n * `178` \n * `179` \n * `180` + \n * `181` \n * `182` \n * `183` \n * `184` \n * `185` \n * `186` \n * `187` + \n * `188` \n * `189` \n * `190` \n * `191` \n * `192` \n * `193` \n * `194` + \n * `195` \n * `196` \n * `197` \n * `198` \n * `199` \n * `200` \n * `201` + \n * `202` \n * `203` \n * `204` \n * `205` \n * `206` \n * `207` \n * `208` + \n * `209` \n * `210` \n * `211` \n * `212` \n * `213` \n * `214` \n * `215` + \n * `216` \n * `217` \n * `218` \n * `219` \n * `220` \n * `221` \n * `222` + \n * `223` \n * `224` \n * `225` \n * `226` \n * `227` \n * `228` \n * `229` + \n * `230` \n * `231` \n * `232` \n * `233` \n * `234` \n * `235` \n * `236` + \n * `237` \n * `238` \n * `239` \n * `240` \n * `241` \n * `242` \n * `243` + \n * `244` \n * `245` \n * `246` \n * `247` \n * `248` \n * `249` \n * `250` + \n * `251` \n * `252` \n * `253` \n * `254` \n * `255` \n * `256` \n * `257` + \n * `258` \n * `259` \n * `260` \n * `261` \n * `262` \n * `263` \n * `264` + \n * `265` \n * `266` \n * `267` \n * `268` \n * `269` \n * `270` \n * `271` + \n * `272` \n * `273` \n * `274` \n * `275` \n * `276` \n * `277` \n * `278` + \n * `279` \n * `280` \n * `281` \n * `282` \n * `283` \n * `284` \n * `285` + \n * `286` \n * `287` \n * `288` \n * `289` \n * `290` \n * `291` \n * `292` + \n * `293` \n * `294` \n * `295` \n * `296` \n * `297` \n * `298` \n * `299` + \n * `300` \n * `301` \n * `302` \n * `303` \n * `304` \n * `305` \n * `306` + \n * `307` \n * `308` \n * `309` \n * `310` \n * `311` \n * `312` \n * `313` + \n * `314` \n * `315` \n * `316` \n * `317` \n * `318` \n * `319` \n * `320` + \n * `321` \n * `322` \n * `323` \n * `324` \n * `325` \n * `326` \n * `327` + \n * `328` \n * `329` \n * `330` \n * `331` \n * `332` \n * `333` \n * `334` + \n * `335` \n * `336` \n * `337` \n * `338` \n * `339` \n * `340` \n * `341` + \n * `342` \n * `343` \n * `344` \n * `345` \n * `346` \n * `347` \n * `348` + \n * `349` \n * `350` \n * `351` \n * `352` \n * `353` \n * `354` \n * `355` + \n * `356` \n * `357` \n * `358` \n * `359` \n * `360` \n * `361` \n * `362` + \n * `363` \n * `364` \n * `365` \n * `366` \n * `367` \n * `368` \n * `369` + \n * `370` \n * `371` \n * `372` \n * `373` \n * `374` \n * `375` \n * `376` + \n * `377` \n * `378` \n * `379` \n * `380` \n * `381` \n * `382` \n * `383` + \n * `384` \n * `385` \n * `386` \n * `387` \n * `388` \n * `389` \n * `390` + \n * `391` \n * `392` \n * `393` \n * `394` \n * `395` \n * `396` \n * `397` + \n * `398` \n * `399` \n * `400` \n * `401` \n * `402` \n * `403` \n * `404` + \n * `405` \n * `406` \n * `407` \n * `408` \n * `409` \n * `410` \n * `411` + \n * `412` \n * `413` \n * `414` \n * `415` \n * `416` \n * `417` \n * `418` + \n * `419` \n * `420` \n * `421` \n * `422` \n * `423` \n * `424` \n * `425` + \n * `426` \n * `427` \n * `428` \n * `429` \n * `430` \n * `431` \n * `432` + \n * `433` \n * `434` \n * `435` \n * `436` \n * `437` \n * `438` \n * `439` + \n * `440` \n * `441` \n * `442` \n * `443` \n * `444` \n * `445` \n * `446` + \n * `447` \n * `448` \n * `449` \n * `450` \n * `451` \n * `452` \n * `453` + \n * `454` \n * `455` \n * `456` \n * `457` \n * `458` \n * `459` \n * `460` + \n * `461` \n * `462` \n * `463` \n * `464` \n * `465` \n * `466` \n * `467` + \n * `468` \n * `469` \n * `470` \n * `471` \n * `472` \n * `473` \n * `474` + \n * `475` \n * `476` \n * `477` \n * `478` \n * `479` \n * `480` \n * `481` + \n * `482` \n * `483` \n * `484` \n * `485` \n * `486` \n * `487` \n * `488` + \n * `489` \n * `490` \n * `491` \n * `492` \n * `493` \n * `494` \n * `495` + \n * `496` \n * `497` \n * `498` \n * `499` \n * `500` \n * `501` \n * `502` + \n * `503` \n * `504` \n * `505` \n * `506` \n * `507` \n * `508` \n * `509` + \n * `510` \n * `511` \n * `512` \n * `513` \n * `514` \n * `515` \n * `516` + \n * `517` \n * `518` \n * `519` \n * `520` \n * `521` \n * `522` \n * `523` + \n * `524` \n * `525` \n * `526` \n * `527` \n * `528` \n * `529` \n * `530` + \n * `531` \n * `532` \n * `533` \n * `534` \n * `535` \n * `536` \n * `537` + \n * `538` \n * `539` \n * `540` \n * `541` \n * `542` \n * `543` \n * `544` + \n * `545` \n * `546` \n * `547` \n * `548` \n * `549` \n * `550` \n * `551` + \n * `552` \n * `553` \n * `554` \n * `555` \n * `556` \n * `557` \n * `558` + \n * `559` \n * `560` \n * `561` \n * `562` \n * `563` \n * `564` \n * `565` + \n * `566` \n * `567` \n * `568` \n * `569` \n * `570` \n * `571` \n * `572` + \n * `573` \n * `574` \n * `575` \n * `576` \n * `577` \n * `578` \n * `579` + \n * `580` \n * `581` \n * `582` \n * `583` \n * `584` \n * `585` \n * `586` + \n * `587` \n * `588` \n * `589` \n * `590` \n * `591` \n * `592` \n * `593` + \n * `594` \n * `595` \n * `596` \n * `597` \n * `598` \n * `599` \n * `600` + \n * `601` \n * `602` \n * `603` \n * `604` \n * `605` \n * `606` \n * `607` + \n * `608` \n * `609` \n * `610` \n * `611` \n * `612` \n * `613` \n * `614` + \n * `615` \n * `616` \n * `617` \n * `618` \n * `619` \n * `620` \n * `621` + \n * `622` \n * `623` \n * `624` \n * `625` \n * `626` \n * `627` \n * `628` + \n * `629` \n * `630` \n * `631` \n * `632` \n * `633` \n * `634` \n * `635` + \n * `636` \n * `637` \n * `638` \n * `639` \n * `640` \n * `641` \n * `642` + \n * `643` \n * `644` \n * `645` \n * `646` \n * `647` \n * `648` \n * `649` + \n * `650` \n * `651` \n * `652` \n * `653` \n * `654` \n * `655` \n * `656` + \n * `657` \n * `658` \n * `659` \n * `660` \n * `661` \n * `662` \n * `663` + \n * `664` \n * `665` \n * `666` \n * `667` \n * `668` \n * `669` \n * `670` + \n * `671` \n * `672` \n * `673` \n * `674` \n * `675` \n * `676` \n * `677` + \n * `678` \n * `679` \n * `680` \n * `681` \n * `682` \n * `683` \n * `684` + \n * `685` \n * `686` \n * `687` \n * `688` \n * `689` \n * `690` \n * `691` + \n * `692` \n * `693` \n * `694` \n * `695` \n * `696` \n * `697` \n * `698` + \n * `699` \n * `700` \n * `701` \n * `702` \n * `703` \n * `704` \n * `705` + \n * `706` \n * `707` \n * `708` \n * `709` \n * `710` \n * `711` \n * `712` + \n * `713` \n * `714` \n * `715` \n * `716` \n * `717` \n * `718` \n * `719` + \n * `720` \n * `721` \n * `722` \n * `723` \n * `724` \n * `725` \n * `726` + \n * `727` \n * `728` \n * `729` \n * `730` \n * `731` \n * `732` \n * `733` + \n * `734` \n * `735` \n * `736` \n * `737` \n * `738` \n * `739` \n * `740` + \n * `741` \n * `742` \n * `743` \n * `744` \n * `745` \n * `746` \n * `747` + \n * `748` \n * `749` \n * `750` \n * `751` \n * `752` \n * `753` \n * `754` + \n * `755` \n * `756` \n * `757` \n * `758` \n * `759` \n * `760` \n * `761` + \n * `762` \n * `763` \n * `764` \n * `765` \n * `766` \n * `767` \n * `768` + \n * `769` \n * `770` \n * `771` \n * `772` \n * `773` \n * `774` \n * `775` + \n * `776` \n * `777` \n * `778` \n * `779` \n * `780` \n * `781` \n * `782` + \n * `783` \n * `784` \n * `785` \n * `786` \n * `787` \n * `788` \n * `789` + \n * `790` \n * `791` \n * `792` \n * `793` \n * `794` \n * `795` \n * `796` + \n * `797` \n * `798` \n * `799` \n * `800` \n * `801` \n * `802` \n * `803` + \n * `804` \n * `805` \n * `806` \n * `807` \n * `808` \n * `809` \n * `810` + \n * `811` \n * `812` \n * `813` \n * `814` \n * `815` \n * `816` \n * `817` + \n * `818` \n * `819` \n * `820` \n * `821` \n * `822` \n * `823` \n * `824` + \n * `825` \n * `826` \n * `827` \n * `828` \n * `829` \n * `830` \n * `831` + \n * `832` \n * `833` \n * `834` \n * `835` \n * `836` \n * `837` \n * `838` + \n * `839` \n * `840` \n * `841` \n * `842` \n * `843` \n * `844` \n * `845` + \n * `846` \n * `847` \n * `848` \n * `849` \n * `850` \n * `851` \n * `852` + \n * `853` \n * `854` \n * `855` \n * `856` \n * `857` \n * `858` \n * `859` + \n * `860` \n * `861` \n * `862` \n * `863` \n * `864` \n * `865` \n * `866` + \n * `867` \n * `868` \n * `869` \n * `870` \n * `871` \n * `872` \n * `873` + \n * `874` \n * `875` \n * `876` \n * `877` \n * `878` \n * `879` \n * `880` + \n * `881` \n * `882` \n * `883` \n * `884` \n * `885` \n * `886` \n * `887` + \n * `888` \n * `889` \n * `890` \n * `891` \n * `892` \n * `893` \n * `894` + \n * `895` \n * `896` \n * `897` \n * `898` \n * `899` \n * `900` \n * `901` + \n * `902` \n * `903` \n * `904` \n * `905` \n * `906` \n * `907` \n * `908` + \n * `909` \n * `910` \n * `911` \n * `912` \n * `913` \n * `914` \n * `915` + \n * `916` \n * `917` \n * `918` \n * `919` \n * `920` \n * `921` \n * `922` + \n * `923` \n * `924` \n * `925` \n * `926` \n * `927` \n * `928` \n * `929` + \n * `930` \n * `931` \n * `932` \n * `933` \n * `934` \n * `935` \n * `936` + \n * `937` \n * `938` \n * `939` \n * `940` \n * `941` \n * `942` \n * `943` + \n * `944` \n * `945` \n * `946` \n * `947` \n * `948` \n * `949` \n * `950` + \n * `951` \n * `952` \n * `953` \n * `954` \n * `955` \n * `956` \n * `957` + \n * `958` \n * `959` \n * `960` \n * `961` \n * `962` \n * `963` \n * `964` + \n * `965` \n * `966` \n * `967` \n * `968` \n * `969` \n * `970` \n * `971` + \n * `972` \n * `973` \n * `974` \n * `975` \n * `976` \n * `977` \n * `978` + \n * `979` \n * `980` \n * `981` \n * `982` \n * `983` \n * `984` \n * `985` + \n * `986` \n * `987` \n * `988` \n * `989` \n * `990` \n * `991` \n * `992` + \n * `993` \n * `994` \n * `995` \n * `996` \n * `997` \n * `998` \n * `999` + \n * `1000` \n " + default: 100 + enum: 1..1000 + required: false + schema: + type: integer + - name: dry_run + in: query + description: Defaults to true for safety reasons. It performs a dry run where + no actual deletion will be performed. Set to false to actually delete the + environment + default: true + required: false + schema: + type: string + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + '400': + description: bad request + '401': + description: unauthorized + '404': + description: not found + '409': + description: conflict + "/api/v4/projects/{id}/environments/stop_stale": + post: + summary: Stop stale environments + tags: + - - environments + parameters: + - name: id + in: path + schema: + oneOf: + - type: string + - type: integer + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + - name: before + in: query + format: date-time + description: Stop all environments that were last modified or deployed to + before this date. required: true schema: type: string + responses: + '204': + description: no content + '400': + description: bad request + '401': + description: unauthorized + "/events": + get: + summary: List currently authenticated users eventss events + tags: + - - events + parameters: + - name: scope + in: query + description: Include all events across a user’s projects + example: all + required: false + schema: + type: string - name: page in: query description: Current page number @@ -125,18 +935,18 @@ paths: author_username: type: string example: root - '404': - description: not found - "/events": + '401': + description: unauthorized + "/users/{id}/events": get: - summary: List currently authenticated users eventss events - tags: [] + summary: Get the contribution events of a specified user + tags: + - - events parameters: - - name: scope - in: query - description: Include all events across a user’s projects - example: all - required: false + - name: id + in: path + description: The ID or username of the user + required: true schema: type: string - name: page @@ -248,7 +1058,7 @@ paths: author_username: type: string example: root - '401': - description: unauthorized + '404': + description: not found servers: - url: http://gdk.test:3000 -- GitLab From cff6c80e1b531bdde13ab374252e264331edb70d Mon Sep 17 00:00:00 2001 From: Vlad Wolanyk Date: Fri, 12 Sep 2025 10:01:25 -0400 Subject: [PATCH 5/6] Update with param schema Enums need schema object for param values to generate drop downs for enums in ui --- lib/grape_to_rswag.rb | 36 +- spec/requests/rswag/audit_events_spec.rb | 46 ++ spec/requests/rswag/environments_spec.rb | 78 +-- spec/requests/rswag/events_spec.rb | 36 +- swagger/v1/swagger.yaml | 844 +++++++++++------------ 5 files changed, 514 insertions(+), 526 deletions(-) create mode 100644 spec/requests/rswag/audit_events_spec.rb diff --git a/lib/grape_to_rswag.rb b/lib/grape_to_rswag.rb index 5ad424b3c5fff6..89792c09794ada 100644 --- a/lib/grape_to_rswag.rb +++ b/lib/grape_to_rswag.rb @@ -247,37 +247,43 @@ def format_parameter_line(param) param_options << "name: #{param[:name].inspect}" param_options << "in: #{param[:in].inspect}" + # Add description if present + param_options << "description: #{param[:description].inspect}" if param[:description] + + # Format the required field + param_options << "required: #{param[:required]}" if param.key?(:required) + + # Build schema hash for OpenAPI 3.0 compliance + schema_parts = [] + # Handle oneOf vs simple types if param[:oneOf] one_of_schemas = param[:oneOf].map(&:inspect).join(', ') - param_options << "schema: { oneOf: [#{one_of_schemas}] }" + schema_parts << "oneOf: [#{one_of_schemas}]" else - param_options << "type: #{param[:type].inspect}" if param[:type] + schema_parts << "type: #{param[:type].inspect}" if param[:type] end - param_options << "format: #{param[:format].inspect}" if param[:format] + schema_parts << "format: #{param[:format].inspect}" if param[:format] # Handle array items if present # grape uses type definitions of Array[Integer] or Array[String] which need a type for the items in the array if param[:items] items_formatted = param[:items].map { |k, v| "#{k}: #{v.inspect}" }.join(', ') - param_options << "items: { #{items_formatted} }" + schema_parts << "items: { #{items_formatted} }" end - # Add description if present - param_options << "description: #{param[:description].inspect}" if param[:description] - - # Add default value if present - param_options << "default: #{param[:default].inspect}" if param.key?(:default) + # Add enum values if present (must be inside schema for OpenAPI 3.0) + schema_parts << "enum: #{param[:enum].inspect}" if param[:enum] - # Add enum values if present - param_options << "enum: #{param[:enum].inspect}" if param[:enum] + # Add example if present (must be inside schema for OpenAPI 3.0) + schema_parts << "example: #{param[:example].inspect}" if param.key?(:example) - # Add example if present - param_options << "example: #{param[:example].inspect}" if param.key?(:example) + # Add default value if present (must be inside schema for OpenAPI 3.0) + schema_parts << "default: #{param[:default].inspect}" if param.key?(:default) - # Format the required field - param_options << "required: #{param[:required]}" if param.key?(:required) + # Wrap all type/format/enum/example/default info in schema object + param_options << "schema: { #{schema_parts.join(', ')} }" " parameter #{param_options.join(', ')}" end diff --git a/spec/requests/rswag/audit_events_spec.rb b/spec/requests/rswag/audit_events_spec.rb new file mode 100644 index 00000000000000..c039cac194cb3e --- /dev/null +++ b/spec/requests/rswag/audit_events_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# rubocop:disable all -- for development + +require 'swagger_helper' + +RSpec.describe "AuditEvents API", type: :request do + + path "/api/v4/audit_events" do + get 'Get the list of audit events' do + tags ["audit_events"] + produces 'application/json' + parameter name: "entity_type", in: :query, description: "Return list of audit events for the specified entity type", required: true, schema: { type: :string, enum: ["Project", "User", "Group", "Gitlab::Audit::InstanceScope"] } + parameter name: "entity_id", in: :query, required: false, schema: { type: :integer } + parameter name: "created_after", in: :query, description: "Return audit events created after the specified time", required: false, schema: { type: :string, format: "date-time", example: "2016-01-19T09:05:50.355Z" } + parameter name: "created_before", in: :query, description: "Return audit events created before the specified time", required: false, schema: { type: :string, format: "date-time", example: "2016-01-19T09:05:50.355Z" } + parameter name: "page", in: :query, description: "Current page number", required: false, schema: { type: :integer, example: 1, default: 1 } + parameter name: "per_page", in: :query, description: "Number of items per page", required: false, schema: { type: :integer, example: 20, default: 20 } + + response '200', 'successful' do + schema type: :array, items: { type: :object } + run_test! + end + response '404', 'not found' do + run_test! + end + end + end + + + path "/api/v4/audit_events/{id}" do + get 'Get single audit event' do + tags ["audit_events"] + produces 'application/json' + parameter name: "id", in: :path, description: "The ID of audit event", required: true, schema: { type: :integer } + + response '200', 'successful' do + schema type: :object + run_test! + end + response '404', 'not found' do + run_test! + end + end + end + +end diff --git a/spec/requests/rswag/environments_spec.rb b/spec/requests/rswag/environments_spec.rb index d581c304e02bf4..c302324798444b 100644 --- a/spec/requests/rswag/environments_spec.rb +++ b/spec/requests/rswag/environments_spec.rb @@ -9,12 +9,12 @@ get 'List environments' do tags ["environments"] produces 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "page", in: :query, type: :integer, description: "Current page number", default: 1, example: 1, required: false - parameter name: "per_page", in: :query, type: :integer, description: "Number of items per page", default: 20, example: 20, required: false - parameter name: "name", in: :query, type: :string, description: "Return the environment with this name. Mutually exclusive with search", required: false - parameter name: "search", in: :query, type: :string, description: "Return list of environments matching the search criteria. Mutually exclusive with name. Must be at least 3 characters.", required: false - parameter name: "states", in: :query, type: :string, description: "List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments", enum: ["stopped", "stopping", "available"], required: false + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "page", in: :query, description: "Current page number", required: false, schema: { type: :integer, example: 1, default: 1 } + parameter name: "per_page", in: :query, description: "Number of items per page", required: false, schema: { type: :integer, example: 20, default: 20 } + parameter name: "name", in: :query, description: "Return the environment with this name. Mutually exclusive with search", required: false, schema: { type: :string } + parameter name: "search", in: :query, description: "Return list of environments matching the search criteria. Mutually exclusive with name. Must be at least 3 characters.", required: false, schema: { type: :string } + parameter name: "states", in: :query, description: "List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments", required: false, schema: { type: :string, enum: ["stopped", "stopping", "available"] } response '200', 'successful' do schema type: :array, items: { @@ -46,16 +46,16 @@ tags ["environments"] produces 'application/json' consumes 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "name", in: :query, type: :string, description: "The name of the environment", required: true - parameter name: "external_url", in: :query, type: :string, description: "Place to link to for this environment", required: false - parameter name: "slug", in: :query, type: :string, required: false - parameter name: "tier", in: :query, type: :string, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", enum: ["production", "staging", "testing", "development", "other"], required: false - parameter name: "cluster_agent_id", in: :query, type: :integer, description: "The ID of the Cluster Agent to associate with this environment", required: false - parameter name: "kubernetes_namespace", in: :query, type: :string, description: "The Kubernetes namespace to associate with this environment", required: false - parameter name: "flux_resource_path", in: :query, type: :string, description: "The Flux resource path to associate with this environment", required: false - parameter name: "description", in: :query, type: :string, description: "The description of the environment", required: false - parameter name: "auto_stop_setting", in: :query, type: :string, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", enum: ["always", "with_action"], required: false + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "name", in: :query, description: "The name of the environment", required: true, schema: { type: :string } + parameter name: "external_url", in: :query, description: "Place to link to for this environment", required: false, schema: { type: :string } + parameter name: "slug", in: :query, required: false, schema: { type: :string } + parameter name: "tier", in: :query, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", required: false, schema: { type: :string, enum: ["production", "staging", "testing", "development", "other"] } + parameter name: "cluster_agent_id", in: :query, description: "The ID of the Cluster Agent to associate with this environment", required: false, schema: { type: :integer } + parameter name: "kubernetes_namespace", in: :query, description: "The Kubernetes namespace to associate with this environment", required: false, schema: { type: :string } + parameter name: "flux_resource_path", in: :query, description: "The Flux resource path to associate with this environment", required: false, schema: { type: :string } + parameter name: "description", in: :query, description: "The description of the environment", required: false, schema: { type: :string } + parameter name: "auto_stop_setting", in: :query, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", required: false, schema: { type: :string, enum: ["always", "with_action"] } response '200', 'successful' do schema type: :object, properties: { @@ -91,16 +91,16 @@ tags ["environments"] produces 'application/json' consumes 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true - parameter name: "external_url", in: :query, type: :string, description: "The new URL on which this deployment is viewable", required: false - parameter name: "slug", in: :query, type: :string, required: false - parameter name: "tier", in: :query, type: :string, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", enum: ["production", "staging", "testing", "development", "other"], required: false - parameter name: "cluster_agent_id", in: :query, type: :integer, description: "The ID of the Cluster Agent to associate with this environment", required: false - parameter name: "kubernetes_namespace", in: :query, type: :string, description: "The Kubernetes namespace to associate with this environment", required: false - parameter name: "flux_resource_path", in: :query, type: :string, description: "The Flux resource path to associate with this environment", required: false - parameter name: "description", in: :query, type: :string, description: "The description of the environment", required: false - parameter name: "auto_stop_setting", in: :query, type: :string, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", enum: ["always", "with_action"], required: false + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "environment_id", in: :path, description: "The ID of the environment", required: true, schema: { type: :integer } + parameter name: "external_url", in: :query, description: "The new URL on which this deployment is viewable", required: false, schema: { type: :string } + parameter name: "slug", in: :query, required: false, schema: { type: :string } + parameter name: "tier", in: :query, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", required: false, schema: { type: :string, enum: ["production", "staging", "testing", "development", "other"] } + parameter name: "cluster_agent_id", in: :query, description: "The ID of the Cluster Agent to associate with this environment", required: false, schema: { type: :integer } + parameter name: "kubernetes_namespace", in: :query, description: "The Kubernetes namespace to associate with this environment", required: false, schema: { type: :string } + parameter name: "flux_resource_path", in: :query, description: "The Flux resource path to associate with this environment", required: false, schema: { type: :string } + parameter name: "description", in: :query, description: "The description of the environment", required: false, schema: { type: :string } + parameter name: "auto_stop_setting", in: :query, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", required: false, schema: { type: :string, enum: ["always", "with_action"] } response '200', 'successful' do schema type: :object, properties: { @@ -131,8 +131,8 @@ delete 'Delete an environment' do tags ["environments"] produces 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "environment_id", in: :path, description: "The ID of the environment", required: true, schema: { type: :integer } response '200', 'successful' do schema type: :object, properties: { @@ -160,8 +160,8 @@ get 'Get a specific environment' do tags ["environments"] produces 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "environment_id", in: :path, description: "The ID of the environment", required: true, schema: { type: :integer } response '200', 'successful' do schema type: :object, properties: { @@ -193,10 +193,10 @@ delete 'Delete multiple stopped review apps' do tags ["environments"] produces 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "before", in: :query, type: :string, format: "date-time", description: "The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)", required: false - parameter name: "limit", in: :query, type: :integer, description: "Maximum number of environments to delete. Defaults to 100", default: 100, enum: 1..1000, required: false - parameter name: "dry_run", in: :query, type: :string, description: "Defaults to true for safety reasons. It performs a dry run where no actual deletion will be performed. Set to false to actually delete the environment", default: true, required: false + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "before", in: :query, description: "The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)", required: false, schema: { type: :string, format: "date-time" } + parameter name: "limit", in: :query, description: "Maximum number of environments to delete. Defaults to 100", required: false, schema: { type: :integer, enum: 1..1000, default: 100 } + parameter name: "dry_run", in: :query, description: "Defaults to true for safety reasons. It performs a dry run where no actual deletion will be performed. Set to false to actually delete the environment", required: false, schema: { type: :string, default: true } response '200', 'successful' do schema type: :object, properties: { @@ -230,9 +230,9 @@ tags ["environments"] produces 'application/json' consumes 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "environment_id", in: :path, type: :integer, description: "The ID of the environment", required: true - parameter name: "force", in: :query, type: :string, description: "Force environment to stop without executing `on_stop` actions", required: false + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "environment_id", in: :path, description: "The ID of the environment", required: true, schema: { type: :integer } + parameter name: "force", in: :query, description: "Force environment to stop without executing `on_stop` actions", required: false, schema: { type: :string } response '200', 'successful' do schema type: :object, properties: { @@ -268,8 +268,8 @@ tags ["environments"] produces 'application/json' consumes 'application/json' - parameter name: "id", in: :path, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] }, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true - parameter name: "before", in: :query, type: :string, format: "date-time", description: "Stop all environments that were last modified or deployed to before this date.", required: true + parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } + parameter name: "before", in: :query, description: "Stop all environments that were last modified or deployed to before this date.", required: true, schema: { type: :string, format: "date-time" } response '204', 'no content' do run_test! diff --git a/spec/requests/rswag/events_spec.rb b/spec/requests/rswag/events_spec.rb index b8c64732c8ad91..acc22df720ea38 100644 --- a/spec/requests/rswag/events_spec.rb +++ b/spec/requests/rswag/events_spec.rb @@ -5,18 +5,18 @@ RSpec.describe "Events API", type: :request do - path "/events" do + path "/api/v4/events" do get 'List currently authenticated users eventss events' do tags ["events"] produces 'application/json' - parameter name: "scope", in: :query, type: :string, description: "Include all events across a user’s projects", example: "all", required: false - parameter name: "page", in: :query, type: :integer, description: "Current page number", default: 1, example: 1, required: false - parameter name: "per_page", in: :query, type: :integer, description: "Number of items per page", default: 20, example: 20, required: false - parameter name: "action", in: :query, type: :string, description: "Event action to filter on", required: false - parameter name: "target_type", in: :query, type: :string, description: "Event target type to filter on", enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false - parameter name: "before", in: :query, type: :string, format: :date, description: "Include only events created before this date", required: false - parameter name: "after", in: :query, type: :string, format: :date, description: "Include only events created after this date", required: false - parameter name: "sort", in: :query, type: :string, description: "Return events sorted in ascending and descending order", default: "desc", enum: ["asc", "desc"], required: false + parameter name: "scope", in: :query, description: "Include all events across a user’s projects", required: false, schema: { type: :string, example: "all" } + parameter name: "page", in: :query, description: "Current page number", required: false, schema: { type: :integer, example: 1, default: 1 } + parameter name: "per_page", in: :query, description: "Number of items per page", required: false, schema: { type: :integer, example: 20, default: 20 } + parameter name: "action", in: :query, description: "Event action to filter on", required: false, schema: { type: :string } + parameter name: "target_type", in: :query, description: "Event target type to filter on", required: false, schema: { type: :string, enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"] } + parameter name: "before", in: :query, description: "Include only events created before this date", required: false, schema: { type: :string, format: :date } + parameter name: "after", in: :query, description: "Include only events created after this date", required: false, schema: { type: :string, format: :date } + parameter name: "sort", in: :query, description: "Return events sorted in ascending and descending order", required: false, schema: { type: :string, enum: ["asc", "desc"], default: "desc" } response '200', 'successful' do schema type: :array, items: { @@ -45,18 +45,18 @@ end - path "/users/{id}/events" do + path "/api/v4/users/{id}/events" do get 'Get the contribution events of a specified user' do tags ["events"] produces 'application/json' - parameter name: "id", in: :path, type: :string, description: "The ID or username of the user", required: true - parameter name: "page", in: :query, type: :integer, description: "Current page number", default: 1, example: 1, required: false - parameter name: "per_page", in: :query, type: :integer, description: "Number of items per page", default: 20, example: 20, required: false - parameter name: "action", in: :query, type: :string, description: "Event action to filter on", required: false - parameter name: "target_type", in: :query, type: :string, description: "Event target type to filter on", enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"], required: false - parameter name: "before", in: :query, type: :string, format: :date, description: "Include only events created before this date", required: false - parameter name: "after", in: :query, type: :string, format: :date, description: "Include only events created after this date", required: false - parameter name: "sort", in: :query, type: :string, description: "Return events sorted in ascending and descending order", default: "desc", enum: ["asc", "desc"], required: false + parameter name: "id", in: :path, description: "The ID or username of the user", required: true, schema: { type: :string } + parameter name: "page", in: :query, description: "Current page number", required: false, schema: { type: :integer, example: 1, default: 1 } + parameter name: "per_page", in: :query, description: "Number of items per page", required: false, schema: { type: :integer, example: 20, default: 20 } + parameter name: "action", in: :query, description: "Event action to filter on", required: false, schema: { type: :string } + parameter name: "target_type", in: :query, description: "Event target type to filter on", required: false, schema: { type: :string, enum: ["issue", "milestone", "merge_request", "note", "project", "snippet", "user", "wiki", "design"] } + parameter name: "before", in: :query, description: "Include only events created before this date", required: false, schema: { type: :string, format: :date } + parameter name: "after", in: :query, description: "Include only events created after this date", required: false, schema: { type: :string, format: :date } + parameter name: "sort", in: :query, description: "Return events sorted in ascending and descending order", required: false, schema: { type: :string, enum: ["asc", "desc"], default: "desc" } response '200', 'successful' do schema type: :array, items: { diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 91496f9c6581e6..e1e3f076f0fbc3 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -4,138 +4,60 @@ info: title: API V1 version: v1 paths: - "/api/v4/projects/{id}/environments/{environment_id}/stop": - post: - summary: Stop an environment + "/api/v4/audit_events": + get: + summary: Get the list of audit events tags: - - - environments + - - audit_events parameters: - - name: id - in: path - schema: - oneOf: - - type: string - - type: integer - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - - name: environment_id - in: path - description: The ID of the environment - required: true - schema: - type: integer - - name: force + - name: entity_type in: query - description: Force environment to stop without executing `on_stop` actions - required: false + description: Return list of audit events for the specified entity type + required: true schema: type: string - responses: - '200': - description: successful - content: - application/json: - schema: - type: object - properties: - id: - type: string - example: 1 - name: - type: string - example: deploy - slug: - type: string - example: deploy - external_url: - type: string - example: https://deploy.gitlab.example.com - created_at: - type: string - example: '2019-05-25T18:55:13.252Z' - updated_at: - type: string - example: '2019-05-25T18:55:13.252Z' - tier: - type: string - example: development - state: - type: string - example: available - auto_stop_at: - type: string - example: '2019-05-25T18:55:13.252Z' - description: - type: string - example: description - auto_stop_setting: - type: string - example: always - '400': - description: bad request - '401': - description: unauthorized - '404': - description: not found - "/api/v4/projects/{id}/environments": - get: - summary: List environments - tags: - - - environments - parameters: - - name: id - in: path - schema: - oneOf: - - type: string - - type: integer - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - - name: page + enum: + - Project + - User + - Group + - Gitlab::Audit::InstanceScope + - name: entity_id in: query - description: Current page number - default: 1 - example: 1 required: false schema: type: integer - - name: per_page + - name: created_after in: query - description: Number of items per page - default: 20 - example: 20 + description: Return audit events created after the specified time required: false schema: - type: integer - - name: name + type: string + format: date-time + example: '2016-01-19T09:05:50.355Z' + - name: created_before in: query - description: Return the environment with this name. Mutually exclusive with - search + description: Return audit events created before the specified time required: false schema: type: string - - name: search + format: date-time + example: '2016-01-19T09:05:50.355Z' + - name: page in: query - description: Return list of environments matching the search criteria. Mutually - exclusive with name. Must be at least 3 characters. + description: Current page number required: false schema: - type: string - - name: states + type: integer + example: 1 + default: 1 + - name: per_page in: query - description: "List all environments that match a specific state. Accepted - values: `available`, `stopping`, or `stopped`. If no state value given, - returns all environments:\n * `stopped` \n * `stopping` \n * `available` - \n " - enum: - - stopped - - stopping - - available + description: Number of items per page required: false schema: - type: string + type: integer + example: 20 + default: 20 responses: '200': description: successful @@ -145,67 +67,53 @@ paths: type: array items: type: object - properties: - id: - type: string - example: 1 - name: - type: string - example: deploy - slug: - type: string - example: deploy - external_url: - type: string - example: https://deploy.gitlab.example.com - created_at: - type: string - example: '2019-05-25T18:55:13.252Z' - updated_at: - type: string - example: '2019-05-25T18:55:13.252Z' - tier: - type: string - example: development - state: - type: string - example: available - auto_stop_at: - type: string - example: '2019-05-25T18:55:13.252Z' - description: - type: string - example: description - auto_stop_setting: - type: string - example: always - '401': - description: unauthorized '404': description: not found - post: - summary: Create a new environment + "/api/v4/audit_events/{id}": + get: + summary: Get single audit event + tags: + - - audit_events + parameters: + - name: id + in: path + description: The ID of audit event + required: true + schema: + type: integer + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + '404': + description: not found + "/api/v4/projects/{id}/environments/{environment_id}": + put: + summary: Update an existing environment tags: - - environments parameters: - name: id in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true schema: oneOf: - type: string - type: integer - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - - name: name - in: query - description: The name of the environment + - name: environment_id + in: path + description: The ID of the environment required: true schema: - type: string + type: integer - name: external_url in: query - description: Place to link to for this environment + description: The new URL on which this deployment is viewable required: false schema: type: string @@ -216,18 +124,17 @@ paths: type: string - name: tier in: query - description: "The tier of the new environment. Allowed values are `production`, - `staging`, `testing`, `development`, and `other`:\n * `production` \n * - `staging` \n * `testing` \n * `development` \n * `other` \n " - enum: - - production - - staging - - testing - - development - - other + description: The tier of the new environment. Allowed values are `production`, + `staging`, `testing`, `development`, and `other` required: false schema: type: string + enum: + - production + - staging + - testing + - development + - other - name: cluster_agent_id in: query description: The ID of the Cluster Agent to associate with this environment @@ -256,12 +163,12 @@ paths: in: query description: The auto stop setting for the environment. Allowed values are `always` and `with_action` - enum: - - always - - with_action required: false schema: type: string + enum: + - always + - with_action responses: '200': description: successful @@ -309,86 +216,26 @@ paths: description: unauthorized '404': description: not found - "/api/v4/projects/{id}/environments/{environment_id}": - put: - summary: Update an existing environment + delete: + summary: Delete an environment tags: - - environments parameters: - name: id in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true schema: oneOf: - type: string - type: integer - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - name: environment_id in: path description: The ID of the environment required: true schema: type: integer - - name: external_url - in: query - description: The new URL on which this deployment is viewable - required: false - schema: - type: string - - name: slug - in: query - required: false - schema: - type: string - - name: tier - in: query - description: "The tier of the new environment. Allowed values are `production`, - `staging`, `testing`, `development`, and `other`:\n * `production` \n * - `staging` \n * `testing` \n * `development` \n * `other` \n " - enum: - - production - - staging - - testing - - development - - other - required: false - schema: - type: string - - name: cluster_agent_id - in: query - description: The ID of the Cluster Agent to associate with this environment - required: false - schema: - type: integer - - name: kubernetes_namespace - in: query - description: The Kubernetes namespace to associate with this environment - required: false - schema: - type: string - - name: flux_resource_path - in: query - description: The Flux resource path to associate with this environment - required: false - schema: - type: string - - name: description - in: query - description: The description of the environment - required: false - schema: - type: string - - name: auto_stop_setting - in: query - description: The auto stop setting for the environment. Allowed values are - `always` and `with_action` - enum: - - always - - with_action - required: false - schema: - type: string responses: '200': description: successful @@ -430,26 +277,24 @@ paths: auto_stop_setting: type: string example: always - '400': - description: bad request '401': description: unauthorized '404': description: not found - delete: - summary: Delete an environment + get: + summary: Get a specific environment tags: - - environments parameters: - name: id in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true schema: oneOf: - type: string - type: integer - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - name: environment_id in: path description: The ID of the environment @@ -501,26 +346,33 @@ paths: description: unauthorized '404': description: not found - get: - summary: Get a specific environment + "/api/v4/projects/{id}/environments/{environment_id}/stop": + post: + summary: Stop an environment tags: - - environments parameters: - name: id in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true schema: oneOf: - type: string - type: integer - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - name: environment_id in: path description: The ID of the environment required: true schema: type: integer + - name: force + in: query + description: Force environment to stop without executing `on_stop` actions + required: false + schema: + type: string responses: '200': description: successful @@ -562,193 +414,82 @@ paths: auto_stop_setting: type: string example: always + '400': + description: bad request '401': description: unauthorized '404': description: not found - "/api/v4/projects/{id}/environments/review_apps": - delete: - summary: Delete multiple stopped review apps + "/api/v4/projects/{id}/environments/stop_stale": + post: + summary: Stop stale environments tags: - - environments parameters: - name: id in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true schema: oneOf: - type: string - type: integer + - name: before + in: query + description: Stop all environments that were last modified or deployed to + before this date. + required: true + schema: + type: string + format: date-time + responses: + '204': + description: no content + '400': + description: bad request + '401': + description: unauthorized + "/api/v4/projects/{id}/environments/review_apps": + delete: + summary: Delete multiple stopped review apps + tags: + - - environments + parameters: + - name: id + in: path description: The ID or URL-encoded path of the project owned by the authenticated user required: true + schema: + oneOf: + - type: string + - type: integer - name: before in: query - format: date-time description: The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) required: false schema: type: string + format: date-time - name: limit in: query - description: "Maximum number of environments to delete. Defaults to 100:\n - * `1` \n * `2` \n * `3` \n * `4` \n * `5` \n * `6` \n * `7` \n * `8` \n - * `9` \n * `10` \n * `11` \n * `12` \n * `13` \n * `14` \n * `15` \n * `16` - \n * `17` \n * `18` \n * `19` \n * `20` \n * `21` \n * `22` \n * `23` \n - * `24` \n * `25` \n * `26` \n * `27` \n * `28` \n * `29` \n * `30` \n * - `31` \n * `32` \n * `33` \n * `34` \n * `35` \n * `36` \n * `37` \n * `38` - \n * `39` \n * `40` \n * `41` \n * `42` \n * `43` \n * `44` \n * `45` \n - * `46` \n * `47` \n * `48` \n * `49` \n * `50` \n * `51` \n * `52` \n * - `53` \n * `54` \n * `55` \n * `56` \n * `57` \n * `58` \n * `59` \n * `60` - \n * `61` \n * `62` \n * `63` \n * `64` \n * `65` \n * `66` \n * `67` \n - * `68` \n * `69` \n * `70` \n * `71` \n * `72` \n * `73` \n * `74` \n * - `75` \n * `76` \n * `77` \n * `78` \n * `79` \n * `80` \n * `81` \n * `82` - \n * `83` \n * `84` \n * `85` \n * `86` \n * `87` \n * `88` \n * `89` \n - * `90` \n * `91` \n * `92` \n * `93` \n * `94` \n * `95` \n * `96` \n * - `97` \n * `98` \n * `99` \n * `100` \n * `101` \n * `102` \n * `103` \n - * `104` \n * `105` \n * `106` \n * `107` \n * `108` \n * `109` \n * `110` - \n * `111` \n * `112` \n * `113` \n * `114` \n * `115` \n * `116` \n * `117` - \n * `118` \n * `119` \n * `120` \n * `121` \n * `122` \n * `123` \n * `124` - \n * `125` \n * `126` \n * `127` \n * `128` \n * `129` \n * `130` \n * `131` - \n * `132` \n * `133` \n * `134` \n * `135` \n * `136` \n * `137` \n * `138` - \n * `139` \n * `140` \n * `141` \n * `142` \n * `143` \n * `144` \n * `145` - \n * `146` \n * `147` \n * `148` \n * `149` \n * `150` \n * `151` \n * `152` - \n * `153` \n * `154` \n * `155` \n * `156` \n * `157` \n * `158` \n * `159` - \n * `160` \n * `161` \n * `162` \n * `163` \n * `164` \n * `165` \n * `166` - \n * `167` \n * `168` \n * `169` \n * `170` \n * `171` \n * `172` \n * `173` - \n * `174` \n * `175` \n * `176` \n * `177` \n * `178` \n * `179` \n * `180` - \n * `181` \n * `182` \n * `183` \n * `184` \n * `185` \n * `186` \n * `187` - \n * `188` \n * `189` \n * `190` \n * `191` \n * `192` \n * `193` \n * `194` - \n * `195` \n * `196` \n * `197` \n * `198` \n * `199` \n * `200` \n * `201` - \n * `202` \n * `203` \n * `204` \n * `205` \n * `206` \n * `207` \n * `208` - \n * `209` \n * `210` \n * `211` \n * `212` \n * `213` \n * `214` \n * `215` - \n * `216` \n * `217` \n * `218` \n * `219` \n * `220` \n * `221` \n * `222` - \n * `223` \n * `224` \n * `225` \n * `226` \n * `227` \n * `228` \n * `229` - \n * `230` \n * `231` \n * `232` \n * `233` \n * `234` \n * `235` \n * `236` - \n * `237` \n * `238` \n * `239` \n * `240` \n * `241` \n * `242` \n * `243` - \n * `244` \n * `245` \n * `246` \n * `247` \n * `248` \n * `249` \n * `250` - \n * `251` \n * `252` \n * `253` \n * `254` \n * `255` \n * `256` \n * `257` - \n * `258` \n * `259` \n * `260` \n * `261` \n * `262` \n * `263` \n * `264` - \n * `265` \n * `266` \n * `267` \n * `268` \n * `269` \n * `270` \n * `271` - \n * `272` \n * `273` \n * `274` \n * `275` \n * `276` \n * `277` \n * `278` - \n * `279` \n * `280` \n * `281` \n * `282` \n * `283` \n * `284` \n * `285` - \n * `286` \n * `287` \n * `288` \n * `289` \n * `290` \n * `291` \n * `292` - \n * `293` \n * `294` \n * `295` \n * `296` \n * `297` \n * `298` \n * `299` - \n * `300` \n * `301` \n * `302` \n * `303` \n * `304` \n * `305` \n * `306` - \n * `307` \n * `308` \n * `309` \n * `310` \n * `311` \n * `312` \n * `313` - \n * `314` \n * `315` \n * `316` \n * `317` \n * `318` \n * `319` \n * `320` - \n * `321` \n * `322` \n * `323` \n * `324` \n * `325` \n * `326` \n * `327` - \n * `328` \n * `329` \n * `330` \n * `331` \n * `332` \n * `333` \n * `334` - \n * `335` \n * `336` \n * `337` \n * `338` \n * `339` \n * `340` \n * `341` - \n * `342` \n * `343` \n * `344` \n * `345` \n * `346` \n * `347` \n * `348` - \n * `349` \n * `350` \n * `351` \n * `352` \n * `353` \n * `354` \n * `355` - \n * `356` \n * `357` \n * `358` \n * `359` \n * `360` \n * `361` \n * `362` - \n * `363` \n * `364` \n * `365` \n * `366` \n * `367` \n * `368` \n * `369` - \n * `370` \n * `371` \n * `372` \n * `373` \n * `374` \n * `375` \n * `376` - \n * `377` \n * `378` \n * `379` \n * `380` \n * `381` \n * `382` \n * `383` - \n * `384` \n * `385` \n * `386` \n * `387` \n * `388` \n * `389` \n * `390` - \n * `391` \n * `392` \n * `393` \n * `394` \n * `395` \n * `396` \n * `397` - \n * `398` \n * `399` \n * `400` \n * `401` \n * `402` \n * `403` \n * `404` - \n * `405` \n * `406` \n * `407` \n * `408` \n * `409` \n * `410` \n * `411` - \n * `412` \n * `413` \n * `414` \n * `415` \n * `416` \n * `417` \n * `418` - \n * `419` \n * `420` \n * `421` \n * `422` \n * `423` \n * `424` \n * `425` - \n * `426` \n * `427` \n * `428` \n * `429` \n * `430` \n * `431` \n * `432` - \n * `433` \n * `434` \n * `435` \n * `436` \n * `437` \n * `438` \n * `439` - \n * `440` \n * `441` \n * `442` \n * `443` \n * `444` \n * `445` \n * `446` - \n * `447` \n * `448` \n * `449` \n * `450` \n * `451` \n * `452` \n * `453` - \n * `454` \n * `455` \n * `456` \n * `457` \n * `458` \n * `459` \n * `460` - \n * `461` \n * `462` \n * `463` \n * `464` \n * `465` \n * `466` \n * `467` - \n * `468` \n * `469` \n * `470` \n * `471` \n * `472` \n * `473` \n * `474` - \n * `475` \n * `476` \n * `477` \n * `478` \n * `479` \n * `480` \n * `481` - \n * `482` \n * `483` \n * `484` \n * `485` \n * `486` \n * `487` \n * `488` - \n * `489` \n * `490` \n * `491` \n * `492` \n * `493` \n * `494` \n * `495` - \n * `496` \n * `497` \n * `498` \n * `499` \n * `500` \n * `501` \n * `502` - \n * `503` \n * `504` \n * `505` \n * `506` \n * `507` \n * `508` \n * `509` - \n * `510` \n * `511` \n * `512` \n * `513` \n * `514` \n * `515` \n * `516` - \n * `517` \n * `518` \n * `519` \n * `520` \n * `521` \n * `522` \n * `523` - \n * `524` \n * `525` \n * `526` \n * `527` \n * `528` \n * `529` \n * `530` - \n * `531` \n * `532` \n * `533` \n * `534` \n * `535` \n * `536` \n * `537` - \n * `538` \n * `539` \n * `540` \n * `541` \n * `542` \n * `543` \n * `544` - \n * `545` \n * `546` \n * `547` \n * `548` \n * `549` \n * `550` \n * `551` - \n * `552` \n * `553` \n * `554` \n * `555` \n * `556` \n * `557` \n * `558` - \n * `559` \n * `560` \n * `561` \n * `562` \n * `563` \n * `564` \n * `565` - \n * `566` \n * `567` \n * `568` \n * `569` \n * `570` \n * `571` \n * `572` - \n * `573` \n * `574` \n * `575` \n * `576` \n * `577` \n * `578` \n * `579` - \n * `580` \n * `581` \n * `582` \n * `583` \n * `584` \n * `585` \n * `586` - \n * `587` \n * `588` \n * `589` \n * `590` \n * `591` \n * `592` \n * `593` - \n * `594` \n * `595` \n * `596` \n * `597` \n * `598` \n * `599` \n * `600` - \n * `601` \n * `602` \n * `603` \n * `604` \n * `605` \n * `606` \n * `607` - \n * `608` \n * `609` \n * `610` \n * `611` \n * `612` \n * `613` \n * `614` - \n * `615` \n * `616` \n * `617` \n * `618` \n * `619` \n * `620` \n * `621` - \n * `622` \n * `623` \n * `624` \n * `625` \n * `626` \n * `627` \n * `628` - \n * `629` \n * `630` \n * `631` \n * `632` \n * `633` \n * `634` \n * `635` - \n * `636` \n * `637` \n * `638` \n * `639` \n * `640` \n * `641` \n * `642` - \n * `643` \n * `644` \n * `645` \n * `646` \n * `647` \n * `648` \n * `649` - \n * `650` \n * `651` \n * `652` \n * `653` \n * `654` \n * `655` \n * `656` - \n * `657` \n * `658` \n * `659` \n * `660` \n * `661` \n * `662` \n * `663` - \n * `664` \n * `665` \n * `666` \n * `667` \n * `668` \n * `669` \n * `670` - \n * `671` \n * `672` \n * `673` \n * `674` \n * `675` \n * `676` \n * `677` - \n * `678` \n * `679` \n * `680` \n * `681` \n * `682` \n * `683` \n * `684` - \n * `685` \n * `686` \n * `687` \n * `688` \n * `689` \n * `690` \n * `691` - \n * `692` \n * `693` \n * `694` \n * `695` \n * `696` \n * `697` \n * `698` - \n * `699` \n * `700` \n * `701` \n * `702` \n * `703` \n * `704` \n * `705` - \n * `706` \n * `707` \n * `708` \n * `709` \n * `710` \n * `711` \n * `712` - \n * `713` \n * `714` \n * `715` \n * `716` \n * `717` \n * `718` \n * `719` - \n * `720` \n * `721` \n * `722` \n * `723` \n * `724` \n * `725` \n * `726` - \n * `727` \n * `728` \n * `729` \n * `730` \n * `731` \n * `732` \n * `733` - \n * `734` \n * `735` \n * `736` \n * `737` \n * `738` \n * `739` \n * `740` - \n * `741` \n * `742` \n * `743` \n * `744` \n * `745` \n * `746` \n * `747` - \n * `748` \n * `749` \n * `750` \n * `751` \n * `752` \n * `753` \n * `754` - \n * `755` \n * `756` \n * `757` \n * `758` \n * `759` \n * `760` \n * `761` - \n * `762` \n * `763` \n * `764` \n * `765` \n * `766` \n * `767` \n * `768` - \n * `769` \n * `770` \n * `771` \n * `772` \n * `773` \n * `774` \n * `775` - \n * `776` \n * `777` \n * `778` \n * `779` \n * `780` \n * `781` \n * `782` - \n * `783` \n * `784` \n * `785` \n * `786` \n * `787` \n * `788` \n * `789` - \n * `790` \n * `791` \n * `792` \n * `793` \n * `794` \n * `795` \n * `796` - \n * `797` \n * `798` \n * `799` \n * `800` \n * `801` \n * `802` \n * `803` - \n * `804` \n * `805` \n * `806` \n * `807` \n * `808` \n * `809` \n * `810` - \n * `811` \n * `812` \n * `813` \n * `814` \n * `815` \n * `816` \n * `817` - \n * `818` \n * `819` \n * `820` \n * `821` \n * `822` \n * `823` \n * `824` - \n * `825` \n * `826` \n * `827` \n * `828` \n * `829` \n * `830` \n * `831` - \n * `832` \n * `833` \n * `834` \n * `835` \n * `836` \n * `837` \n * `838` - \n * `839` \n * `840` \n * `841` \n * `842` \n * `843` \n * `844` \n * `845` - \n * `846` \n * `847` \n * `848` \n * `849` \n * `850` \n * `851` \n * `852` - \n * `853` \n * `854` \n * `855` \n * `856` \n * `857` \n * `858` \n * `859` - \n * `860` \n * `861` \n * `862` \n * `863` \n * `864` \n * `865` \n * `866` - \n * `867` \n * `868` \n * `869` \n * `870` \n * `871` \n * `872` \n * `873` - \n * `874` \n * `875` \n * `876` \n * `877` \n * `878` \n * `879` \n * `880` - \n * `881` \n * `882` \n * `883` \n * `884` \n * `885` \n * `886` \n * `887` - \n * `888` \n * `889` \n * `890` \n * `891` \n * `892` \n * `893` \n * `894` - \n * `895` \n * `896` \n * `897` \n * `898` \n * `899` \n * `900` \n * `901` - \n * `902` \n * `903` \n * `904` \n * `905` \n * `906` \n * `907` \n * `908` - \n * `909` \n * `910` \n * `911` \n * `912` \n * `913` \n * `914` \n * `915` - \n * `916` \n * `917` \n * `918` \n * `919` \n * `920` \n * `921` \n * `922` - \n * `923` \n * `924` \n * `925` \n * `926` \n * `927` \n * `928` \n * `929` - \n * `930` \n * `931` \n * `932` \n * `933` \n * `934` \n * `935` \n * `936` - \n * `937` \n * `938` \n * `939` \n * `940` \n * `941` \n * `942` \n * `943` - \n * `944` \n * `945` \n * `946` \n * `947` \n * `948` \n * `949` \n * `950` - \n * `951` \n * `952` \n * `953` \n * `954` \n * `955` \n * `956` \n * `957` - \n * `958` \n * `959` \n * `960` \n * `961` \n * `962` \n * `963` \n * `964` - \n * `965` \n * `966` \n * `967` \n * `968` \n * `969` \n * `970` \n * `971` - \n * `972` \n * `973` \n * `974` \n * `975` \n * `976` \n * `977` \n * `978` - \n * `979` \n * `980` \n * `981` \n * `982` \n * `983` \n * `984` \n * `985` - \n * `986` \n * `987` \n * `988` \n * `989` \n * `990` \n * `991` \n * `992` - \n * `993` \n * `994` \n * `995` \n * `996` \n * `997` \n * `998` \n * `999` - \n * `1000` \n " - default: 100 - enum: 1..1000 + description: Maximum number of environments to delete. Defaults to 100 required: false schema: type: integer + enum: 1..1000 + default: 100 - name: dry_run in: query description: Defaults to true for safety reasons. It performs a dry run where no actual deletion will be performed. Set to false to actually delete the environment - default: true required: false schema: type: string + default: true responses: '200': description: successful @@ -783,37 +524,236 @@ paths: description: not found '409': description: conflict - "/api/v4/projects/{id}/environments/stop_stale": - post: - summary: Stop stale environments + "/api/v4/projects/{id}/environments": + get: + summary: List environments tags: - - environments parameters: - name: id in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true schema: oneOf: - type: string - type: integer + - name: page + in: query + description: Current page number + required: false + schema: + type: integer + example: 1 + default: 1 + - name: per_page + in: query + description: Number of items per page + required: false + schema: + type: integer + example: 20 + default: 20 + - name: name + in: query + description: Return the environment with this name. Mutually exclusive with + search + required: false + schema: + type: string + - name: search + in: query + description: Return list of environments matching the search criteria. Mutually + exclusive with name. Must be at least 3 characters. + required: false + schema: + type: string + - name: states + in: query + description: 'List all environments that match a specific state. Accepted + values: `available`, `stopping`, or `stopped`. If no state value given, + returns all environments' + required: false + schema: + type: string + enum: + - stopped + - stopping + - available + responses: + '200': + description: successful + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '401': + description: unauthorized + '404': + description: not found + post: + summary: Create a new environment + tags: + - - environments + parameters: + - name: id + in: path description: The ID or URL-encoded path of the project owned by the authenticated user required: true - - name: before + schema: + oneOf: + - type: string + - type: integer + - name: name in: query - format: date-time - description: Stop all environments that were last modified or deployed to - before this date. + description: The name of the environment required: true schema: type: string + - name: external_url + in: query + description: Place to link to for this environment + required: false + schema: + type: string + - name: slug + in: query + required: false + schema: + type: string + - name: tier + in: query + description: The tier of the new environment. Allowed values are `production`, + `staging`, `testing`, `development`, and `other` + required: false + schema: + type: string + enum: + - production + - staging + - testing + - development + - other + - name: cluster_agent_id + in: query + description: The ID of the Cluster Agent to associate with this environment + required: false + schema: + type: integer + - name: kubernetes_namespace + in: query + description: The Kubernetes namespace to associate with this environment + required: false + schema: + type: string + - name: flux_resource_path + in: query + description: The Flux resource path to associate with this environment + required: false + schema: + type: string + - name: description + in: query + description: The description of the environment + required: false + schema: + type: string + - name: auto_stop_setting + in: query + description: The auto stop setting for the environment. Allowed values are + `always` and `with_action` + required: false + schema: + type: string + enum: + - always + - with_action responses: - '204': - description: no content + '200': + description: successful + content: + application/json: + schema: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always '400': description: bad request '401': description: unauthorized - "/events": + '404': + description: not found + "/api/v4/events": get: summary: List currently authenticated users eventss events tags: @@ -822,26 +762,26 @@ paths: - name: scope in: query description: Include all events across a user’s projects - example: all required: false schema: type: string + example: all - name: page in: query description: Current page number - default: 1 - example: 1 required: false schema: type: integer + example: 1 + default: 1 - name: per_page in: query description: Number of items per page - default: 20 - example: 20 required: false schema: type: integer + example: 20 + default: 20 - name: action in: query description: Event action to filter on @@ -850,46 +790,44 @@ paths: type: string - name: target_type in: query - description: "Event target type to filter on:\n * `issue` \n * `milestone` - \n * `merge_request` \n * `note` \n * `project` \n * `snippet` \n * `user` - \n * `wiki` \n * `design` \n " - enum: - - issue - - milestone - - merge_request - - note - - project - - snippet - - user - - wiki - - design + description: Event target type to filter on required: false schema: type: string + enum: + - issue + - milestone + - merge_request + - note + - project + - snippet + - user + - wiki + - design - name: before in: query - format: date description: Include only events created before this date required: false schema: type: string + format: date - name: after in: query - format: date description: Include only events created after this date required: false schema: type: string + format: date - name: sort in: query description: Return events sorted in ascending and descending order - default: desc - enum: - - asc - - desc required: false schema: type: string + enum: + - asc + - desc + default: desc responses: '200': description: successful @@ -937,7 +875,7 @@ paths: example: root '401': description: unauthorized - "/users/{id}/events": + "/api/v4/users/{id}/events": get: summary: Get the contribution events of a specified user tags: @@ -952,19 +890,19 @@ paths: - name: page in: query description: Current page number - default: 1 - example: 1 required: false schema: type: integer + example: 1 + default: 1 - name: per_page in: query description: Number of items per page - default: 20 - example: 20 required: false schema: type: integer + example: 20 + default: 20 - name: action in: query description: Event action to filter on @@ -973,46 +911,44 @@ paths: type: string - name: target_type in: query - description: "Event target type to filter on:\n * `issue` \n * `milestone` - \n * `merge_request` \n * `note` \n * `project` \n * `snippet` \n * `user` - \n * `wiki` \n * `design` \n " - enum: - - issue - - milestone - - merge_request - - note - - project - - snippet - - user - - wiki - - design + description: Event target type to filter on required: false schema: type: string + enum: + - issue + - milestone + - merge_request + - note + - project + - snippet + - user + - wiki + - design - name: before in: query - format: date description: Include only events created before this date required: false schema: type: string + format: date - name: after in: query - format: date description: Include only events created after this date required: false schema: type: string + format: date - name: sort in: query description: Return events sorted in ascending and descending order - default: desc - enum: - - asc - - desc required: false schema: type: string + enum: + - asc + - desc + default: desc responses: '200': description: successful -- GitLab From f5be4978191d35aa0ed9c3da2d4c72b337af0e20 Mon Sep 17 00:00:00 2001 From: Vlad Wolanyk Date: Sat, 13 Sep 2025 18:52:48 -0400 Subject: [PATCH 6/6] Fix Request Body --- lib/grape_to_rswag.rb | 60 +-- spec/requests/rswag/environments_spec.rb | 69 ++- swagger/v1/swagger.yaml | 585 ++++++++++------------- 3 files changed, 326 insertions(+), 388 deletions(-) diff --git a/lib/grape_to_rswag.rb b/lib/grape_to_rswag.rb index 89792c09794ada..b4d72e9a9bb80d 100644 --- a/lib/grape_to_rswag.rb +++ b/lib/grape_to_rswag.rb @@ -134,7 +134,7 @@ def build_request_body(params) <<~RUBY.chomp - request_body_json schema: { + parameter name: :body, in: :body, schema: { type: :object, properties: { #{properties} @@ -158,6 +158,8 @@ def format_params(route) # just in case return (this may not be needed) return params unless route.params + request_method = route.attributes.request_method.downcase + # Takes care of path parameters path_params = route.path.scan(/:(\w+)/).flatten - %w[version format] path_params.each do |param_name| @@ -198,40 +200,44 @@ def format_params(route) params << param_hash end - # select and format query params ==> in: :query - query_params = route.params.reject { |name, _| path_params.include?(name.to_s) } - query_params.each do |name, details| + # Only add query params for GET requests + # For POST/PUT/PATCH, non-path params should go in request body instead + if request_method == 'get' + # select and format query params ==> in: :query + query_params = route.params.reject { |name, _| path_params.include?(name.to_s) } + query_params.each do |name, details| - param_hash = { - name: name.to_s, - in: :query - } + param_hash = { + name: name.to_s, + in: :query + } - # Merge the type information properly - type_info = grape_type_to_openapi(details[:type]) - param_hash.merge!(type_info) + # Merge the type information properly + type_info = grape_type_to_openapi(details[:type]) + param_hash.merge!(type_info) - # Add description from Grape params - param_hash[:description] = details[:desc] if details[:desc] + # Add description from Grape params + param_hash[:description] = details[:desc] if details[:desc] - # Add default value if present - # There is an unhandled issue here where we do have some dynamic defaults defined - # as procs ie in environment there is a default: -> { 30.days.ago } - # for this POC choosing to ignore this edge case - param_hash[:default] = details[:default] if details[:default] && !details[:default].is_a?(Proc) + # Add default value if present + # There is an unhandled issue here where we do have some dynamic defaults defined + # as procs ie in environment there is a default: -> { 30.days.ago } + # for this POC choosing to ignore this edge case + param_hash[:default] = details[:default] if details[:default] && !details[:default].is_a?(Proc) - # Add enum values if present - param_hash[:enum] = details[:values] if details[:values] + # Add enum values if present + param_hash[:enum] = details[:values] if details[:values] - # Add example from documentation if present - if details[:documentation] && details[:documentation][:example] - param_hash[:example] = details[:documentation][:example] - end + # Add example from documentation if present + if details[:documentation] && details[:documentation][:example] + param_hash[:example] = details[:documentation][:example] + end - # Add required field - param_hash[:required] = details[:required] || false + # Add required field + param_hash[:required] = details[:required] || false - params << param_hash + params << param_hash + end end params diff --git a/spec/requests/rswag/environments_spec.rb b/spec/requests/rswag/environments_spec.rb index c302324798444b..ee22d505431dcc 100644 --- a/spec/requests/rswag/environments_spec.rb +++ b/spec/requests/rswag/environments_spec.rb @@ -47,15 +47,22 @@ produces 'application/json' consumes 'application/json' parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } - parameter name: "name", in: :query, description: "The name of the environment", required: true, schema: { type: :string } - parameter name: "external_url", in: :query, description: "Place to link to for this environment", required: false, schema: { type: :string } - parameter name: "slug", in: :query, required: false, schema: { type: :string } - parameter name: "tier", in: :query, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", required: false, schema: { type: :string, enum: ["production", "staging", "testing", "development", "other"] } - parameter name: "cluster_agent_id", in: :query, description: "The ID of the Cluster Agent to associate with this environment", required: false, schema: { type: :integer } - parameter name: "kubernetes_namespace", in: :query, description: "The Kubernetes namespace to associate with this environment", required: false, schema: { type: :string } - parameter name: "flux_resource_path", in: :query, description: "The Flux resource path to associate with this environment", required: false, schema: { type: :string } - parameter name: "description", in: :query, description: "The description of the environment", required: false, schema: { type: :string } - parameter name: "auto_stop_setting", in: :query, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", required: false, schema: { type: :string, enum: ["always", "with_action"] } + + parameter name: :body, in: :body, schema: { + type: :object, + properties: { + name: { type: :string }, + external_url: { type: :string }, + slug: { type: :string }, + tier: { type: :string }, + cluster_agent_id: { type: :integer }, + kubernetes_namespace: { type: :string }, + flux_resource_path: { type: :string }, + description: { type: :string }, + auto_stop_setting: { type: :string } + }, + required: [:name] + } response '200', 'successful' do schema type: :object, properties: { @@ -93,14 +100,21 @@ consumes 'application/json' parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } parameter name: "environment_id", in: :path, description: "The ID of the environment", required: true, schema: { type: :integer } - parameter name: "external_url", in: :query, description: "The new URL on which this deployment is viewable", required: false, schema: { type: :string } - parameter name: "slug", in: :query, required: false, schema: { type: :string } - parameter name: "tier", in: :query, description: "The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`", required: false, schema: { type: :string, enum: ["production", "staging", "testing", "development", "other"] } - parameter name: "cluster_agent_id", in: :query, description: "The ID of the Cluster Agent to associate with this environment", required: false, schema: { type: :integer } - parameter name: "kubernetes_namespace", in: :query, description: "The Kubernetes namespace to associate with this environment", required: false, schema: { type: :string } - parameter name: "flux_resource_path", in: :query, description: "The Flux resource path to associate with this environment", required: false, schema: { type: :string } - parameter name: "description", in: :query, description: "The description of the environment", required: false, schema: { type: :string } - parameter name: "auto_stop_setting", in: :query, description: "The auto stop setting for the environment. Allowed values are `always` and `with_action`", required: false, schema: { type: :string, enum: ["always", "with_action"] } + + parameter name: :body, in: :body, schema: { + type: :object, + properties: { + external_url: { type: :string }, + slug: { type: :string }, + tier: { type: :string }, + cluster_agent_id: { type: :integer }, + kubernetes_namespace: { type: :string }, + flux_resource_path: { type: :string }, + description: { type: :string }, + auto_stop_setting: { type: :string } + }, + required: [] + } response '200', 'successful' do schema type: :object, properties: { @@ -194,9 +208,6 @@ tags ["environments"] produces 'application/json' parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } - parameter name: "before", in: :query, description: "The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)", required: false, schema: { type: :string, format: "date-time" } - parameter name: "limit", in: :query, description: "Maximum number of environments to delete. Defaults to 100", required: false, schema: { type: :integer, enum: 1..1000, default: 100 } - parameter name: "dry_run", in: :query, description: "Defaults to true for safety reasons. It performs a dry run where no actual deletion will be performed. Set to false to actually delete the environment", required: false, schema: { type: :string, default: true } response '200', 'successful' do schema type: :object, properties: { @@ -232,7 +243,14 @@ consumes 'application/json' parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } parameter name: "environment_id", in: :path, description: "The ID of the environment", required: true, schema: { type: :integer } - parameter name: "force", in: :query, description: "Force environment to stop without executing `on_stop` actions", required: false, schema: { type: :string } + + parameter name: :body, in: :body, schema: { + type: :object, + properties: { + force: { type: :string } + }, + required: [] + } response '200', 'successful' do schema type: :object, properties: { @@ -269,7 +287,14 @@ produces 'application/json' consumes 'application/json' parameter name: "id", in: :path, description: "The ID or URL-encoded path of the project owned by the authenticated user", required: true, schema: { oneOf: [{:type=>:string}, {:type=>:integer}] } - parameter name: "before", in: :query, description: "Stop all environments that were last modified or deployed to before this date.", required: true, schema: { type: :string, format: "date-time" } + + parameter name: :body, in: :body, schema: { + type: :object, + properties: { + before: { type: :string } + }, + required: [:before] + } response '204', 'no content' do run_test! diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index e1e3f076f0fbc3..263dbf65babb6c 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -4,6 +4,27 @@ info: title: API V1 version: v1 paths: + "/api/v4/audit_events/{id}": + get: + summary: Get single audit event + tags: + - - audit_events + parameters: + - name: id + in: path + description: The ID of audit event + required: true + schema: + type: integer + responses: + '200': + description: successful + content: + application/json: + schema: + type: object + '404': + description: not found "/api/v4/audit_events": get: summary: Get the list of audit events @@ -69,30 +90,9 @@ paths: type: object '404': description: not found - "/api/v4/audit_events/{id}": + "/api/v4/projects/{id}/environments": get: - summary: Get single audit event - tags: - - - audit_events - parameters: - - name: id - in: path - description: The ID of audit event - required: true - schema: - type: integer - responses: - '200': - description: successful - content: - application/json: - schema: - type: object - '404': - description: not found - "/api/v4/projects/{id}/environments/{environment_id}": - put: - summary: Update an existing environment + summary: List environments tags: - - environments parameters: @@ -105,70 +105,109 @@ paths: oneOf: - type: string - type: integer - - name: environment_id - in: path - description: The ID of the environment - required: true - schema: - type: integer - - name: external_url - in: query - description: The new URL on which this deployment is viewable - required: false - schema: - type: string - - name: slug - in: query - required: false - schema: - type: string - - name: tier - in: query - description: The tier of the new environment. Allowed values are `production`, - `staging`, `testing`, `development`, and `other` - required: false - schema: - type: string - enum: - - production - - staging - - testing - - development - - other - - name: cluster_agent_id + - name: page in: query - description: The ID of the Cluster Agent to associate with this environment + description: Current page number required: false schema: type: integer - - name: kubernetes_namespace + example: 1 + default: 1 + - name: per_page in: query - description: The Kubernetes namespace to associate with this environment + description: Number of items per page required: false schema: - type: string - - name: flux_resource_path + type: integer + example: 20 + default: 20 + - name: name in: query - description: The Flux resource path to associate with this environment + description: Return the environment with this name. Mutually exclusive with + search required: false schema: type: string - - name: description + - name: search in: query - description: The description of the environment + description: Return list of environments matching the search criteria. Mutually + exclusive with name. Must be at least 3 characters. required: false schema: type: string - - name: auto_stop_setting + - name: states in: query - description: The auto stop setting for the environment. Allowed values are - `always` and `with_action` + description: 'List all environments that match a specific state. Accepted + values: `available`, `stopping`, or `stopped`. If no state value given, + returns all environments' required: false schema: type: string enum: - - always - - with_action + - stopped + - stopping + - available + responses: + '200': + description: successful + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + example: 1 + name: + type: string + example: deploy + slug: + type: string + example: deploy + external_url: + type: string + example: https://deploy.gitlab.example.com + created_at: + type: string + example: '2019-05-25T18:55:13.252Z' + updated_at: + type: string + example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always + '401': + description: unauthorized + '404': + description: not found + post: + summary: Create a new environment + tags: + - - environments + parameters: + - name: id + in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + schema: + oneOf: + - type: string + - type: integer responses: '200': description: successful @@ -216,8 +255,67 @@ paths: description: unauthorized '404': description: not found - delete: - summary: Delete an environment + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + external_url: + type: string + slug: + type: string + tier: + type: string + cluster_agent_id: + type: integer + kubernetes_namespace: + type: string + flux_resource_path: + type: string + description: + type: string + auto_stop_setting: + type: string + required: + - name + "/api/v4/projects/{id}/environments/stop_stale": + post: + summary: Stop stale environments + tags: + - - environments + parameters: + - name: id + in: path + description: The ID or URL-encoded path of the project owned by the authenticated + user + required: true + schema: + oneOf: + - type: string + - type: integer + responses: + '204': + description: no content + '400': + description: bad request + '401': + description: unauthorized + requestBody: + content: + application/json: + schema: + type: object + properties: + before: + type: string + required: + - before + "/api/v4/projects/{id}/environments/{environment_id}": + put: + summary: Update an existing environment tags: - - environments parameters: @@ -277,12 +375,37 @@ paths: auto_stop_setting: type: string example: always + '400': + description: bad request '401': description: unauthorized '404': description: not found - get: - summary: Get a specific environment + requestBody: + content: + application/json: + schema: + type: object + properties: + external_url: + type: string + slug: + type: string + tier: + type: string + cluster_agent_id: + type: integer + kubernetes_namespace: + type: string + flux_resource_path: + type: string + description: + type: string + auto_stop_setting: + type: string + required: [] + delete: + summary: Delete an environment tags: - - environments parameters: @@ -346,9 +469,8 @@ paths: description: unauthorized '404': description: not found - "/api/v4/projects/{id}/environments/{environment_id}/stop": - post: - summary: Stop an environment + get: + summary: Get a specific environment tags: - - environments parameters: @@ -367,12 +489,6 @@ paths: required: true schema: type: integer - - name: force - in: query - description: Force environment to stop without executing `on_stop` actions - required: false - schema: - type: string responses: '200': description: successful @@ -414,15 +530,13 @@ paths: auto_stop_setting: type: string example: always - '400': - description: bad request '401': description: unauthorized '404': description: not found - "/api/v4/projects/{id}/environments/stop_stale": + "/api/v4/projects/{id}/environments/{environment_id}/stop": post: - summary: Stop stale environments + summary: Stop an environment tags: - - environments parameters: @@ -435,61 +549,12 @@ paths: oneOf: - type: string - type: integer - - name: before - in: query - description: Stop all environments that were last modified or deployed to - before this date. - required: true - schema: - type: string - format: date-time - responses: - '204': - description: no content - '400': - description: bad request - '401': - description: unauthorized - "/api/v4/projects/{id}/environments/review_apps": - delete: - summary: Delete multiple stopped review apps - tags: - - - environments - parameters: - - name: id + - name: environment_id in: path - description: The ID or URL-encoded path of the project owned by the authenticated - user + description: The ID of the environment required: true - schema: - oneOf: - - type: string - - type: integer - - name: before - in: query - description: The date before which environments can be deleted. Defaults to - 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`) - required: false - schema: - type: string - format: date-time - - name: limit - in: query - description: Maximum number of environments to delete. Defaults to 100 - required: false schema: type: integer - enum: 1..1000 - default: 100 - - name: dry_run - in: query - description: Defaults to true for safety reasons. It performs a dry run where - no actual deletion will be performed. Set to false to actually delete the - environment - required: false - schema: - type: string - default: true responses: '200': description: successful @@ -516,120 +581,39 @@ paths: updated_at: type: string example: '2019-05-25T18:55:13.252Z' + tier: + type: string + example: development + state: + type: string + example: available + auto_stop_at: + type: string + example: '2019-05-25T18:55:13.252Z' + description: + type: string + example: description + auto_stop_setting: + type: string + example: always '400': description: bad request '401': description: unauthorized '404': description: not found - '409': - description: conflict - "/api/v4/projects/{id}/environments": - get: - summary: List environments - tags: - - - environments - parameters: - - name: id - in: path - description: The ID or URL-encoded path of the project owned by the authenticated - user - required: true - schema: - oneOf: - - type: string - - type: integer - - name: page - in: query - description: Current page number - required: false - schema: - type: integer - example: 1 - default: 1 - - name: per_page - in: query - description: Number of items per page - required: false - schema: - type: integer - example: 20 - default: 20 - - name: name - in: query - description: Return the environment with this name. Mutually exclusive with - search - required: false - schema: - type: string - - name: search - in: query - description: Return list of environments matching the search criteria. Mutually - exclusive with name. Must be at least 3 characters. - required: false - schema: - type: string - - name: states - in: query - description: 'List all environments that match a specific state. Accepted - values: `available`, `stopping`, or `stopped`. If no state value given, - returns all environments' - required: false - schema: - type: string - enum: - - stopped - - stopping - - available - responses: - '200': - description: successful - content: - application/json: - schema: - type: array - items: - type: object - properties: - id: - type: string - example: 1 - name: - type: string - example: deploy - slug: - type: string - example: deploy - external_url: - type: string - example: https://deploy.gitlab.example.com - created_at: - type: string - example: '2019-05-25T18:55:13.252Z' - updated_at: - type: string - example: '2019-05-25T18:55:13.252Z' - tier: - type: string - example: development - state: - type: string - example: available - auto_stop_at: - type: string - example: '2019-05-25T18:55:13.252Z' - description: - type: string - example: description - auto_stop_setting: - type: string - example: always - '401': - description: unauthorized - '404': - description: not found - post: - summary: Create a new environment + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: string + required: [] + "/api/v4/projects/{id}/environments/review_apps": + delete: + summary: Delete multiple stopped review apps tags: - - environments parameters: @@ -642,70 +626,6 @@ paths: oneOf: - type: string - type: integer - - name: name - in: query - description: The name of the environment - required: true - schema: - type: string - - name: external_url - in: query - description: Place to link to for this environment - required: false - schema: - type: string - - name: slug - in: query - required: false - schema: - type: string - - name: tier - in: query - description: The tier of the new environment. Allowed values are `production`, - `staging`, `testing`, `development`, and `other` - required: false - schema: - type: string - enum: - - production - - staging - - testing - - development - - other - - name: cluster_agent_id - in: query - description: The ID of the Cluster Agent to associate with this environment - required: false - schema: - type: integer - - name: kubernetes_namespace - in: query - description: The Kubernetes namespace to associate with this environment - required: false - schema: - type: string - - name: flux_resource_path - in: query - description: The Flux resource path to associate with this environment - required: false - schema: - type: string - - name: description - in: query - description: The description of the environment - required: false - schema: - type: string - - name: auto_stop_setting - in: query - description: The auto stop setting for the environment. Allowed values are - `always` and `with_action` - required: false - schema: - type: string - enum: - - always - - with_action responses: '200': description: successful @@ -732,40 +652,26 @@ paths: updated_at: type: string example: '2019-05-25T18:55:13.252Z' - tier: - type: string - example: development - state: - type: string - example: available - auto_stop_at: - type: string - example: '2019-05-25T18:55:13.252Z' - description: - type: string - example: description - auto_stop_setting: - type: string - example: always '400': description: bad request '401': description: unauthorized '404': description: not found - "/api/v4/events": + '409': + description: conflict + "/api/v4/users/{id}/events": get: - summary: List currently authenticated users eventss events + summary: Get the contribution events of a specified user tags: - - events parameters: - - name: scope - in: query - description: Include all events across a user’s projects - required: false + - name: id + in: path + description: The ID or username of the user + required: true schema: type: string - example: all - name: page in: query description: Current page number @@ -873,20 +779,21 @@ paths: author_username: type: string example: root - '401': - description: unauthorized - "/api/v4/users/{id}/events": + '404': + description: not found + "/api/v4/events": get: - summary: Get the contribution events of a specified user + summary: List currently authenticated users eventss events tags: - - events parameters: - - name: id - in: path - description: The ID or username of the user - required: true + - name: scope + in: query + description: Include all events across a user’s projects + required: false schema: type: string + example: all - name: page in: query description: Current page number @@ -994,7 +901,7 @@ paths: author_username: type: string example: root - '404': - description: not found + '401': + description: unauthorized servers: - url: http://gdk.test:3000 -- GitLab