docbrown/spec/swagger_helper.rb

491 lines
21 KiB
Ruby

require "rails_helper"
# rubocop:disable Layout/LineLength
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.swagger_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 swagger_root
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe "...", swagger_doc: "v2/swagger.json"
config.swagger_docs = {
"v1/api_v1.json" => {
openapi: "3.0.3",
info: {
title: "Forem API V1",
version: "1.0.0",
description: "Access Forem articles, users and other resources via API.
For a real-world example of Forem in action, check out [DEV](https://www.dev.to).
All endpoints can be accessed with the 'api-key' header and a accept header, but
some of them are accessible publicly without authentication.
Dates and date times, unless otherwise specified, must be in
the [RFC 3339](https://tools.ietf.org/html/rfc3339) format."
},
paths: {},
servers: [
{
url: "https://dev.to/api",
description: "Production server"
},
],
security: [{ "api-key": [] }],
components: {
securitySchemes: {
"api-key": {
type: :apiKey,
name: "api-key",
in: :header,
description: "API Key authentication.
Authentication for some endpoints, like write operations on the
Articles API require a DEV API key.
All authenticated endpoints are CORS disabled, the API key is intended for non-browser scripts.
### Getting an API key
To obtain one, please follow these steps:
- visit https://dev.to/settings/extensions
- in the \"DEV API Keys\" section create a new key by adding a
description and clicking on \"Generate API Key\"
![obtain a DEV API Key](https://user-images.githubusercontent.com/37842/172718105-bd93664e-76e0-477d-99c4-265dda0b06c5.png)
- You'll see the newly generated key in the same view
![generated DEV API Key](https://user-images.githubusercontent.com/37842/172718151-e7fe26a0-9937-42e8-96c6-333acdab9e49.png)"
}
},
parameters: {
pageParam: {
in: :query,
name: :page,
required: false,
description: "Pagination page",
schema: {
type: :integer,
format: :int32,
minimum: 1,
default: 1
}
},
perPageParam10to1000: {
in: :query,
name: :per_page,
required: false,
description: "Page size (the number of items to return per page). \
The default maximum value can be overridden by \"API_PER_PAGE_MAX\" environment variable.",
schema: {
type: :integer,
format: :int32,
minimum: 1,
maximum: 1000,
default: 10
}
},
perPageParam24to1000: {
in: :query,
name: :per_page,
required: false,
description: "Page size (the number of items to return per page). \
The default maximum value can be overridden by \"API_PER_PAGE_MAX\" environment variable.",
schema: {
type: :integer,
format: :int32,
minimum: 1,
maximum: 1000,
default: 24
}
},
perPageParam30to1000: {
in: :query,
name: :per_page,
required: false,
description: "Page size (the number of items to return per page). \
The default maximum value can be overridden by \"API_PER_PAGE_MAX\" environment variable.",
schema: {
type: :integer,
format: :int32,
minimum: 1,
maximum: 1000,
default: 30
}
},
perPageParam30to100: {
in: :query,
name: :per_page,
required: false,
description: "Page size (the number of items to return per page). \
The default maximum value can be overridden by \"API_PER_PAGE_MAX\" environment variable.",
schema: {
type: :integer,
format: :int32,
minimum: 1,
maximum: 100,
default: 30
}
},
perPageParam80to1000: {
in: :query,
name: :per_page,
required: false,
description: "Page size (the number of items to return per page). \
The default maximum value can be overridden by \"API_PER_PAGE_MAX\" environment variable.",
schema: {
type: :integer,
format: :int32,
minimum: 1,
maximum: 1000,
default: 80
}
},
listingCategoryParam: {
name: :category,
in: :query,
description: "Using this parameter will return listings belonging to the
requested category.",
schema: {
type: :string
},
example: "cfp"
}
},
schemas: {
ArticleFlareTag: {
description: "Flare tag of the article",
type: :object,
properties: {
name: { type: :string },
bg_color_hex: { description: "Background color (hexadecimal)", type: :string, nullable: true },
text_color_hex: { description: "Text color (hexadecimal)", type: :string, nullable: true }
}
},
ArticleIndex: {
description: "Representation of an article or post returned in a list",
type: :object,
properties: {
type_of: { type: :string },
id: { type: :integer, format: :int32 },
title: { type: :string },
description: { type: :string },
cover_image: { type: :string, format: :url, nullable: true },
readable_publish_date: { type: :string },
social_image: { type: :string, format: :url },
tag_list: { type: :array, items: {
type: :string
} },
tags: { type: :string },
slug: { type: :string },
path: { type: :string, format: "path" },
url: { type: :string, format: :url },
canonical_url: { type: :string, format: :url },
positive_reactions_count: { type: :integer, format: :int32 },
public_reactions_count: { type: :integer, format: :int32 },
created_at: { type: :string, format: "date-time" },
edited_at: { type: :string, format: "date-time", nullable: true },
crossposted_at: { type: :string, format: "date-time", nullable: true },
published_at: { type: :string, format: "date-time" },
last_comment_at: { type: :string, format: "date-time" },
published_timestamp: { description: "Crossposting or published date time", type: :string,
format: "date-time" },
reading_time_minutes: { description: "Reading time, in minutes", type: :integer, format: :int32 },
user: { "$ref": "#/components/schemas/SharedUser" },
flare_tag: { "$ref": "#/components/schemas/ArticleFlareTag" },
organization: { "$ref": "#/components/schemas/SharedOrganization" }
},
required: %w[type_of id title description cover_image readable_publish_date
social_image tag_list tags slug path url canonical_url comments_count
positive_reactions_count public_reactions_count created_at edited_at
crossposted_at published_at last_comment_at published_timestamp user
reading_time_minutes]
},
VideoArticle: {
description: "Representation of an Article with video",
type: :object,
properties: {
type_of: { type: :string },
id: { type: :integer, format: :int64 },
path: { type: :string },
cloudinary_video_url: { type: :string },
title: { type: :string },
user_id: { type: :integer, format: :int64 },
video_duration_in_minutes: { type: :string },
video_source_url: { type: :string },
user: {
description: "Author of the article",
type: :object,
properties: {
name: { type: :string }
}
}
}
},
Article: {
description: "Representation of an Article to be created/updated",
type: :object,
properties: {
article: {
type: :object,
properties: {
title: { type: :string },
body_markdown: { type: :string },
published: { type: :boolean, default: false },
series: { type: :string, nullable: true },
main_image: { type: :string, nullable: true },
canonical_url: { type: :string, nullable: true },
description: { type: :string },
tags: { type: :string },
organization_id: { type: :integer, nullable: true }
}
}
}
},
Organization: {
description: "Representation of an Organization",
type: :object,
properties: {
type_of: { type: :string },
username: { type: :string },
name: { type: :string },
summary: { type: :string },
twitter_username: { type: :string },
github_username: { type: :string },
url: { type: :string },
location: { type: :string },
joined_at: { type: :string },
tech_stack: { type: :string },
tag_line: { type: :string, nullable: true },
story: { type: :string, nullable: true }
}
},
FollowedTag: {
description: "Representation of a followed tag",
type: :object,
properties: {
id: { description: "Tag id", type: :integer, format: :int64 },
name: { type: :string },
points: { type: :number, format: :float }
},
required: %w[id name points]
},
Tag: {
description: "Representation of a tag",
type: :object,
properties: {
id: { description: "Tag id", type: :integer, format: :int64 },
name: { type: :string },
bg_color_hex: { type: :string, nullable: true },
text_color_hex: { type: :string, nullable: true }
}
},
Page: {
description: "Representation of a page object",
type: :object,
properties: {
title: { type: :string, description: "Title of the page" },
slug: { type: :string, description: "Used to link to this page in URLs, must be unique and URL-safe" },
description: { type: :string, description: "For internal use, helps similar pages from one another" },
body_markdown: { type: :string, description: "The text (in markdown) of the ad (required)",
nullable: true },
body_json: { type: :string, description: "For JSON pages, the JSON body", nullable: true },
is_top_level_path: { type: :boolean,
description: "If true, the page is available at '/{slug}' instead of '/page/{slug}', use with caution" },
social_image: { type: :object, nullable: true },
template: {
type: :string, enum: Page::TEMPLATE_OPTIONS, default: "contained",
description: "Controls what kind of layout the page is rendered in"
}
},
required: %w[title slug description template]
},
PodcastEpisodeIndex: {
description: "Representation of a podcast episode returned in a list",
type: :object,
properties: {
type_of: { type: :string },
id: { type: :integer, format: :int32 },
class_name: { type: :string },
path: { type: :string, format: "path" },
title: { type: :string },
image_url: { description: "Podcast episode image url or podcast image url", type: :string, format: :url },
podcast: { "$ref": "#/components/schemas/SharedPodcast" }
},
required: %w[type_of class_name id path title image_url podcast]
},
ProfileImage: {
description: "A profile image object",
type: :object,
properties: {
type_of: { description: "Return profile_image", type: :string },
image_of: { description: "Determines the type of the profile image owner (user or organization)",
type: :string },
profile_image: { description: "Profile image (640x640)", type: :string },
profile_image_90: { description: "Profile image (90x90)", type: :string }
}
},
SharedUser: {
description: "The resource creator",
type: :object,
properties: {
name: { type: :string },
username: { type: :string },
twitter_username: { type: :string, nullable: true },
github_username: { type: :string, nullable: true },
website_url: { type: :string, format: :url, nullable: true },
profile_image: { description: "Profile image (640x640)", type: :string },
profile_image_90: { description: "Profile image (90x90)", type: :string }
}
},
SharedOrganization: {
description: "The organization the resource belongs to",
type: :object,
properties: {
name: { type: :string },
username: { type: :string },
slug: { type: :string },
profile_image: { description: "Profile image (640x640)", type: :string, format: :url },
profile_image_90: { description: "Profile image (90x90)", type: :string, format: :url }
}
},
User: {
description: "The representation of a user",
type: :object,
properties: {
type_of: { type: :string },
id: { type: :integer, format: :int64 },
username: { type: :string },
name: { type: :string },
summary: { type: :string, nullable: true },
twitter_username: { type: :string },
github_username: { type: :string },
website_url: { type: :string, nullable: true },
location: { type: :string, nullable: true },
joined_at: { type: :string },
profile_image: { type: :string }
}
},
SharedPodcast: {
description: "The podcast that the resource belongs to",
type: :object,
properties: {
title: { type: :string },
slug: { type: :string },
image_url: { description: "Podcast image url", type: :string, format: :url }
}
},
Comment: {
description: "A Comment on an Article or Podcast Episode",
type: :object,
properties: {
type_of: { type: :string },
id_code: { type: :string },
created_at: { type: :string, format: "date-time" },
image_url: { description: "Podcast image url", type: :string, format: :url }
}
},
UserInviteParam: {
description: "User invite parameters",
type: :object,
properties: {
email: { type: :string },
name: { type: :string, nullable: true }
}
},
Billboard: {
description: "Billboard, aka Widget, ex. Display Ad",
type: :object,
properties: {
id: { type: :integer, description: "The ID of the Billboard" },
name: { type: :string, description: "For internal use, helps distinguish ads from one another" },
body_markdown: { type: :string, description: "The text (in markdown) of the ad (required)" },
approved: { type: :boolean, description: "Ad must be both published and approved to be in rotation" },
published: { type: :boolean, description: "Ad must be both published and approved to be in rotation" },
organization_id: { type: :integer, description: "Identifies the organization to which the ad belongs", nullable: true },
creator_id: { type: :integer, description: "Identifies the user who created the ad.", nullable: true },
placement_area: { type: :string, enum: Billboard::ALLOWED_PLACEMENT_AREAS,
description: "Identifies which area of site layout the ad can appear in" },
tag_list: { type: :string, description: "Tags on which this ad can be displayed (blank is all/any tags)" },
exclude_article_ids: { type: :string,
nullable: true,
description: "Articles this ad should *not* appear on (blank means no articles are disallowed, and this ad can appear next to any/all articles). Comma-separated list of integer Article IDs" }, # rubocop:disable Layout/LineLength
audience_segment_id: { type: :integer,
description: "Specifies a specific audience segment who will see this billboard" },
audience_segment_type: { type: :string,
enum: AudienceSegment.type_ofs.keys,
description: "Specifies a group of users who will see this billboard (must match audience_segment_id if both provided)" },
target_geolocations: { type: :array,
items: { type: :string },
description: "Locations to show this billboard in (blank means it will be shown in all locations). Specified as a comma-separated list or array of ISO 3166-2 country and optionally region codes)" },
display_to: { type: :string, enum: Billboard.display_tos.keys, default: "all",
description: "Potentially limits visitors to whom the ad is visible" },
type_of: { type: :string, enum: Billboard.type_ofs.keys, default: "in_house",
description: <<~DESCRIBE
Types of the billboards:
in_house (created by admins),
community (created by an entity, appears on entity's content),
external ( created by an entity, or a non-entity, can appear everywhere)
DESCRIBE
}
},
required: %w[name body_markdown placement_area]
},
Segment: {
description: "A manually managed audience segment",
type: "object",
properties: {
id: { type: :integer, description: "The ID of the segment" },
type_of: { type: :string, enum: ["manual"], default: "manual", description: "Marks the segment as manually managed (other types are internal)" },
user_count: { type: :integer, description: "The current number of users in the segment" }
}
},
SegmentUserIds: {
type: "object",
properties: {
user_ids: {
type: :array,
items: { type: :integer },
maxItems: 10_000
}
}
}
}
}
}
}
# Specify the format of the output Swagger file when running "rswag:specs:swaggerize".
# The swagger_docs 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.swagger_format = :json
end
# Convenience method for creating an example section for a response section
module Rswag
module Specs
module ExampleGroupHelpers
def add_examples
after do |example|
# No metadata to generate for empty responses like 201 and 204.
next unless respond_to?(:response) && response&.body.present?
# Generate the examples for the API docs.
example.metadata[:response][:content] = {
"application/json" => {
example: JSON.parse(response.body, symbolize_names: true)
}
}
end
end
end
end
end
# rubocop:enable Layout/LineLength