-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate deployment client from the OpenAPI spec #1149
Changes from 19 commits
c143e5b
7f20689
8d598a7
2115054
b4c9150
064312d
dd01147
a4ff65a
1a6f2a9
a35a39e
981ea24
c03e16b
d57dc8b
5e96813
8f1d6a8
0341041
d66a9fc
b75da94
b6240cd
34a1af7
b6138af
b4ac31c
cf46711
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,91 @@ | ||
module Octokit | ||
class Client | ||
|
||
# Methods for the Deployments API | ||
# | ||
# @see https://developer.github.com/v3/repos/commits/deployments/ | ||
# @see https://developer.github.com/v3/repos/deployments/ | ||
module Deployments | ||
|
||
# Fetch a single deployment for a repository | ||
# Get a single deployment | ||
# | ||
# @param repo [Integer, String, Repository, Hash] A GitHub repository | ||
# @param deployment_id [Integer, String, Repository, Hash] A GitHub repository | ||
# @param deployment_id [Integer] The ID of the deployment | ||
# @return <Sawyer::Resource> A single deployment | ||
# @see https://developer.github.com/v3/repos/deployments/#get-a-single-deployment | ||
def deployment(repo, deployment_id, options = {}) | ||
get("#{Repository.path repo}/deployments/#{deployment_id}", options) | ||
get "#{Repository.path repo}/deployments/#{deployment_id}", options | ||
end | ||
|
||
# List all deployments for a repository | ||
# List deployments | ||
# | ||
# @param repo [Integer, String, Repository, Hash] A GitHub repository | ||
# @param options [String] :sha The SHA recorded at creation time. | ||
# @param options [String] :ref The name of the ref. This can be a branch, tag, or SHA. | ||
# @param options [String] :task The name of the task for the deployment (e.g., `deploy` or `deploy:migrations`). | ||
# @param options [String] :environment The name of the environment that was deployed to (e.g., `staging` or `production`). | ||
# @return [Array<Sawyer::Resource>] A list of deployments | ||
# @see https://developer.github.com/v3/repos/deployments/#list-deployments | ||
def deployments(repo, options = {}) | ||
get("#{Repository.path repo}/deployments", options) | ||
get "#{Repository.path repo}/deployments", options | ||
end | ||
alias :list_deployments :deployments | ||
|
||
# Create a deployment for a ref | ||
# Create a deployment | ||
# | ||
# @param repo [Integer, String, Repository, Hash] A GitHub repository | ||
# @param ref [String] The ref to deploy | ||
# @option options [String] :task Used by the deployment system to allow different execution paths. Defaults to "deploy". | ||
# @option options [String] :payload Meta info about the deployment | ||
# @option options [Boolean] :auto_merge Optional parameter to merge the default branch into the requested deployment branch if necessary. Default: true | ||
# @option options [Array<String>] :required_contexts Optional array of status contexts verified against commit status checks. | ||
# @option options [String] :environment Optional name for the target deployment environment (e.g., production, staging, qa). Default: "production" | ||
# @option options [String] :description Optional short description. | ||
# @return [Sawyer::Resource] A deployment | ||
# @param ref [String] The ref to deploy. This can be a branch, tag, or SHA. | ||
# @param options [String] :task Specifies a task to execute (e.g., `deploy` or `deploy:migrations`). | ||
# @param options [Boolean] :auto_merge Attempts to automatically merge the default branch into the requested ref, if it's behind the default branch. | ||
# @param options [Array] :required_contexts The [status](https://developer.github.com/v3/repos/statuses/) contexts to verify against commit status checks. If you omit this parameter, GitHub verifies all unique contexts before creating a deployment. To bypass checking entirely, pass an empty array. Defaults to all unique contexts. | ||
# @param options [String] :payload JSON payload with extra information about the deployment. | ||
# @param options [String] :environment Name for the target deployment environment (e.g., `production`, `staging`, `qa`). | ||
# @param options [String] :description Short description of the deployment. | ||
# @param options [Boolean] :transient_environment Specifies if the given environment is specific to the deployment and will no longer exist at some point in the future. Default: `false` **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. | ||
# @param options [Boolean] :production_environment Specifies if the given environment is one that end-users directly interact with. Default: `true` when `environment` is `production` and `false` otherwise. **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. | ||
# @return <Sawyer::Resource> The new deployment | ||
# @see https://developer.github.com/v3/repos/deployments/#create-a-deployment | ||
def create_deployment(repo, ref, options = {}) | ||
options[:ref] = ref | ||
post("#{Repository.path repo}/deployments", options) | ||
post "#{Repository.path repo}/deployments", options | ||
end | ||
|
||
# List all statuses for a Deployment | ||
# Get a single deployment status | ||
# | ||
# @param deployment_url [String] A URL for a deployment resource | ||
# @param repo [Integer, String, Repository, Hash] A GitHub repository | ||
# @param deployment_id [Integer] The ID of the deployment | ||
# @param status_id [Integer] The ID of the status | ||
# @return <Sawyer::Resource> A single deployment status | ||
# @see https://developer.github.com/v3/repos/deployments/#get-a-single-deployment-status | ||
def deployment_status(repo, deployment_id, status_id, options = {}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just making a note that this is a breaking change, but we're ok with that. It matches the rest of the client. |
||
get "#{Repository.path repo}/deployments/#{deployment_id}/statuses/#{status_id}", options | ||
end | ||
|
||
# List deployment statuses | ||
# | ||
# @param repo [Integer, String, Repository, Hash] A GitHub repository | ||
# @param deployment_id [Integer] The ID of the deployment | ||
# @return [Array<Sawyer::Resource>] A list of deployment statuses | ||
# @see https://developer.github.com/v3/repos/deployments/#list-deployment-statuses | ||
def deployment_statuses(deployment_url, options = {}) | ||
indigok marked this conversation as resolved.
Show resolved
Hide resolved
|
||
deployment = get(deployment_url, :accept => options[:accept]) | ||
get(deployment.rels[:statuses].href, options) | ||
def deployment_statuses(repo, deployment_id, options = {}) | ||
get "#{Repository.path repo}/deployments/#{deployment_id}/statuses", options | ||
end | ||
alias :list_deployment_statuses :deployment_statuses | ||
|
||
# Create a deployment status for a Deployment | ||
# Create a deployment status | ||
# | ||
# @param deployment_url [String] A URL for a deployment resource | ||
# @param state [String] The state: pending, success, failure, error | ||
# @option options [String] :target_url The target URL to associate with this status. Default: "" | ||
# @option options [String] :description A short description of the status. Maximum length of 140 characters. Default: "" | ||
# @return [Sawyer::Resource] A deployment status | ||
# @param repo [Integer, String, Repository, Hash] A GitHub repository | ||
# @param deployment_id [Integer] The ID of the deployment | ||
# @param state [String] The state of the status. Can be one of `error`, `failure`, `inactive`, `in_progress`, `queued` `pending`, or `success`. **Note:** To use the `inactive` state, you must provide the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. To use the `in_progress` and `queued` states, you must provide the [`application/vnd.github.flash-preview+json`](https://developer.github.com/v3/previews/#deployment-statuses) custom media type. | ||
# @param options [String] :target_url The target URL to associate with this status. This URL should contain output to keep the user updated while the task is running or serve as historical information for what happened in the deployment. **Note:** It's recommended to use the `log_url` parameter, which replaces `target_url`. | ||
# @param options [String] :log_url The full URL of the deployment's output. This parameter replaces `target_url`. We will continue to accept `target_url` to support legacy uses, but we recommend replacing `target_url` with `log_url`. Setting `log_url` will automatically set `target_url` to the same value. Default: `""` **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. | ||
# @param options [String] :description A short description of the status. The maximum description length is 140 characters. | ||
# @param options [String] :environment Name for the target deployment environment, which can be changed when setting a deploy status. For example, `production`, `staging`, or `qa`. **Note:** This parameter requires you to use the [`application/vnd.github.flash-preview+json`](https://developer.github.com/v3/previews/#deployment-statuses) custom media type. | ||
# @param options [String] :environment_url Sets the URL for accessing your environment. Default: `""` **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. | ||
# @param options [Boolean] :auto_inactive Adds a new `inactive` status to all prior non-transient, non-production environment deployments with the same repository and `environment` name as the created status's deployment. An `inactive` status is only added to deployments that had a `success` state. Default: `true` **Note:** To add an `inactive` status to `production` environments, you must use the [`application/vnd.github.flash-preview+json`](https://developer.github.com/v3/previews/#deployment-statuses) custom media type. **Note:** This parameter requires you to use the [`application/vnd.github.ant-man-preview+json`](https://developer.github.com/v3/previews/#enhanced-deployments) custom media type. | ||
# @return <Sawyer::Resource> The new deployment status | ||
# @see https://developer.github.com/v3/repos/deployments/#create-a-deployment-status | ||
def create_deployment_status(deployment_url, state, options = {}) | ||
deployment = get(deployment_url, :accept => options[:accept]) | ||
def create_deployment_status(repo, deployment_id, state, options = {}) | ||
options[:state] = state.to_s.downcase | ||
post(deployment.rels[:statuses].href, options) | ||
post "#{Repository.path repo}/deployments/#{deployment_id}/statuses", options | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
require 'forwardable' | ||
indigok marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require 'json' | ||
require 'pathname' | ||
require 'active_support/inflector' | ||
require 'oas_parser' | ||
|
||
module OpenAPIClientGenerator | ||
|
||
class Endpoint | ||
class PositionalParameterizer | ||
def parameterize(args) | ||
"#{args.join(", ")}, options = {}" | ||
end | ||
end | ||
|
||
class KwargsParameterizer | ||
def parameterize(args) | ||
"#{args.map {|arg| arg + ":"}.join(", ")}, **options" | ||
end | ||
end | ||
|
||
extend Forwardable | ||
|
||
VERB_PRIORITY = %w(GET POST) | ||
|
||
attr_reader :definition, :parameterizer | ||
def initialize(oas_endpoint, parameterizer: OpenAPIClientGenerator::Endpoint::PositionalParameterizer) | ||
@definition = oas_endpoint | ||
@parameterizer = parameterizer.new | ||
end | ||
|
||
def to_s | ||
[ | ||
tomdoc, | ||
method_definition, | ||
alias_definition, | ||
].compact.join("\n") | ||
end | ||
|
||
def tomdoc | ||
<<-TOMDOC.chomp | ||
# #{definition.summary} | ||
# | ||
# #{parameter_documentation.join("\n # ")} | ||
# @return #{return_type_description} #{return_value_description} | ||
# @see #{definition.raw["externalDocs"]["url"]} | ||
TOMDOC | ||
end | ||
|
||
def method_definition | ||
<<-DEF.chomp | ||
def #{method_name}(#{parameters}) | ||
#{method_implementation} | ||
end | ||
DEF | ||
end | ||
|
||
def alias_definition | ||
return unless alternate_name | ||
" alias :#{alternate_name} :#{method_name}" | ||
end | ||
|
||
def singular? | ||
definition.path.path.split("/").last.include? "id" | ||
end | ||
|
||
def method_implementation | ||
[ | ||
*option_overrides, | ||
api_call, | ||
].reject(&:empty?).join("\n ") | ||
end | ||
|
||
def option_overrides | ||
required_params.reject do |param| | ||
param.name == "repo" || param.name.include?("id") | ||
end.map do |param| | ||
normalization = "" | ||
if !!param.enum | ||
normalization = ".to_s.downcase" | ||
end | ||
"options[:#{param.name}] = #{param.name}#{normalization}" | ||
end | ||
end | ||
|
||
def api_call | ||
"#{definition.method} \"#{api_path}\", options" | ||
end | ||
|
||
def api_path | ||
path = definition.path.path.gsub("/repos/{owner}/{repo}", "\#{Repository.path repo}") | ||
path = required_params.reduce(path) do |path, param| | ||
path.gsub("{#{param.name}}", "\#{#{param.name}}") | ||
end | ||
end | ||
|
||
def return_type_description | ||
if verb == "GET" && !singular? | ||
"[Array<Sawyer::Resource>]" | ||
else | ||
"<Sawyer::Resource>" | ||
end | ||
end | ||
|
||
def required_params | ||
params = definition.parameters.select(&:required).reject {|param| ["owner", "accept"].include?(param.name)} | ||
if definition.request_body | ||
params += definition.request_body.properties_for_format("application/json").select { |param| definition.request_body.content["application/json"]["schema"]["required"].include? param.name } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is pretty long, can we make this a multiline block? |
||
end | ||
params | ||
end | ||
|
||
def optional_params | ||
params = definition.parameters.reject(&:required).reject {|param| ["accept", "per_page", "page"].include?(param.name)} | ||
if definition.request_body | ||
params += definition.request_body.properties_for_format("application/json").reject { |param| definition.request_body.content["application/json"]["schema"]["required"].include? param.name } | ||
end | ||
params | ||
end | ||
|
||
def parameter_type(param) | ||
{ | ||
"repo" => "[Integer, String, Repository, Hash]", | ||
}[param.name] || "[#{param.type.capitalize}]" | ||
end | ||
|
||
def parameter_description(param) | ||
return "A GitHub repository" if param.name == "repo" | ||
return "The ID of the #{param.name.gsub("_id", "").gsub("_", " ")}" if param.name.end_with? "_id" | ||
return param.description.gsub("\n", "") | ||
end | ||
|
||
def parameter_documentation | ||
required_params.map {|param| | ||
"@param #{param.name} #{parameter_type(param)} #{parameter_description(param)}" | ||
} + optional_params.map {|param| | ||
"@param options [#{param.type.capitalize}] :#{param.name} #{param.description.gsub("\n", "")}" | ||
} | ||
end | ||
|
||
def return_value_description | ||
case verb | ||
when "GET" | ||
if singular? | ||
"A single #{namespace.gsub("_", " ")}" | ||
else | ||
"A list of #{namespace.gsub("_", " ")}" | ||
end | ||
when "POST" | ||
"The new #{namespace.singularize.gsub("_", " ")}" | ||
else | ||
end | ||
end | ||
|
||
def verb | ||
definition.method.upcase | ||
end | ||
|
||
def parameters | ||
parameterizer.parameterize(required_params.map(&:name)) | ||
end | ||
|
||
def namespace | ||
definition.operation_id.split("/").last.split("-").drop(1).join("_") | ||
end | ||
|
||
def method_name | ||
case verb | ||
when "GET" | ||
namespace | ||
when "POST" | ||
"create_#{namespace}" | ||
else | ||
end | ||
end | ||
|
||
def alternate_name | ||
return unless verb == "GET" | ||
return if singular? | ||
"list_#{namespace}" | ||
end | ||
|
||
def parts | ||
definition.path.path.split("/").reject { |segment| segment.include? "id" } | ||
end | ||
|
||
def priority | ||
[parts.count, VERB_PRIORITY.index(verb), singular?? 0 : 1] | ||
end | ||
end | ||
|
||
class API | ||
def self.at(definition, parameterizer: OpenAPIClientGenerator::Endpoint::PositionalParameterizer) | ||
grouped_paths = definition.paths.group_by do |oas_path| | ||
resource_for_path(oas_path.path) | ||
end | ||
grouped_paths.delete(:unsupported) | ||
grouped_paths.each do |resource, paths| | ||
endpoints = paths.each_with_object([]) do |path, arr| | ||
path.endpoints.each do |endpoint| | ||
arr << OpenAPIClientGenerator::Endpoint.new(endpoint, parameterizer: parameterizer) | ||
end | ||
end | ||
yield new(resource, endpoints: endpoints) | ||
end | ||
end | ||
|
||
def self.resource_for_path(path) | ||
return :unsupported unless path.include? "deployment" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this into an Array? So that we can just add more resources easier? |
||
path_segments = path.split("/").reject{ |segment| segment == "" } | ||
resource = path_segments[3] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems pretty magic, does this ever change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It definitely will, but it's pretty consistent for the paths under "repos" which was my next focus. I can look further into it, but I felt like it was good enough for now 🤔 |
||
resource ||= "repositories" | ||
end | ||
|
||
attr_reader :resource, :endpoints | ||
def initialize(resource, endpoints: []) | ||
@resource = resource | ||
@endpoints = endpoints | ||
end | ||
|
||
def documentation_url | ||
endpoints.first.definition.raw["externalDocs"]["url"].gsub(/#.*/, "") | ||
end | ||
|
||
def to_s | ||
<<-FILE | ||
module Octokit | ||
class Client | ||
# Methods for the #{resource.capitalize} API | ||
# | ||
# @see #{documentation_url} | ||
module #{resource.capitalize} | ||
|
||
#{endpoints.sort_by(&:priority).join("\n\n")} | ||
end | ||
end | ||
end | ||
FILE | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This depends on the right path, but since we're still developing this I think it's alright.