Skip to content

Commit

Permalink
API Generator
Browse files Browse the repository at this point in the history
Signed-off-by: Theo Truong <[email protected]>
  • Loading branch information
nhtruong committed Dec 11, 2024
1 parent c1a9cc9 commit c06bb81
Show file tree
Hide file tree
Showing 647 changed files with 13,112 additions and 24,240 deletions.
63 changes: 63 additions & 0 deletions .github/workflows/generate_api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Generate API from Spec
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 0" # Every Sunday at midnight GMT
jobs:
generate-api:
if: ${{ github.repository == 'opensearch-project/opensearch-ruby' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./api_generator
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

- name: Config git to rebase
run: git config --global pull.rebase true

- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Update bundler
run: |
sudo apt-get update
bundle install
- name: Generate API
run: |-
bundle exec rake download_spec
bundle exec rake generate_api
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_ENV

- name: GitHub App token
id: github_app_token
uses: tibdex/[email protected]
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.github_app_token.outputs.token }}
commit-message: "Updated opensearch-ruby to reflect the latest OpenSearch API spec (${{ env.date }})"
title: "[AUTOCUT] Update opensearch-ruby to reflect the latest OpenSearch API spec (${{ env.date }})"
body: |
Update `opensearch-ruby` to reflect the latest [OpenSearch API spec](https://github.com/opensearch-project/opensearch-api-specification/releases/download/main-latest/opensearch-openapi.yaml).
Date: ${{ env.date }}
branch: update-api-from-spec-${{ env.date }}
base: main
signoff: true
labels: |
autocut
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ coverage
docs/
rdoc/
tmp
Gemfile.lock
/Gemfile.lock
.DS_Store
*.log
.idea/*
profile/**/data/*.json
.byebug_history
dist/
gem-private_key.pem
.ruby-version
/.ruby-version
*/.idea/**
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ Layout/FirstArgumentIndentation:

Layout/FirstArrayElementIndentation:
Enabled: false

Layout/LineLength:
Enabled: false
1 change: 1 addition & 0 deletions api_generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
opensearch-openapi.yaml
19 changes: 19 additions & 0 deletions api_generator/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require: rubocop-rake
AllCops:
Include:
- 'lib/**/*.rb'
- 'Rakefile'
NewCops: enable

Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/AbcSize:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false
Style/MultilineBlockChain:
Enabled: false
1 change: 1 addition & 0 deletions api_generator/.ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.1.0
13 changes: 13 additions & 0 deletions api_generator/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require 'rake'
require 'active_support/all'

task :download_spec do
sh 'curl -L -X GET "https://github.com/opensearch-project/opensearch-api-specification/releases/download/main-latest/opensearch-openapi.yaml" -o opensearch-openapi.yaml'
end


task :generate_api do
require './lib/generator'
generator = Generator.new('./opensearch-openapi.yaml', '../')
generator.generate
end
16 changes: 16 additions & 0 deletions api_generator/gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.

# frozen_string_literal: true

source 'https://rubygems.org'

gem 'rake'
gem 'rubocop', '~> 1.44', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop-rake', require: false
gem 'mustache', '~> 1'
gem 'activesupport', '~> 7'
71 changes: 71 additions & 0 deletions api_generator/gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (7.2.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
ast (2.4.2)
base64 (0.2.0)
bigdecimal (3.1.8)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
drb (2.2.1)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
json (2.7.2)
language_server-protocol (3.17.0.3)
logger (1.6.1)
minitest (5.25.1)
mustache (1.1.1)
parallel (1.26.3)
parser (3.3.5.0)
ast (~> 2.4.1)
racc
racc (1.8.1)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.9.2)
rubocop (1.66.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.3)
parser (>= 3.3.1.0)
rubocop-rake (0.6.0)
rubocop (~> 1.0)
rubocop-rspec (2.11.1)
rubocop (~> 1.19)
ruby-progressbar (1.13.0)
securerandom (0.3.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.6.0)

PLATFORMS
ruby
x86_64-darwin-22

DEPENDENCIES
activesupport (~> 7)
mustache (~> 1)
rake
rubocop (~> 1.44)
rubocop-rake
rubocop-rspec

BUNDLED WITH
2.5.3
2 changes: 2 additions & 0 deletions api_generator/lib/_generated_code_warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This file is generated from the OpenSearch REST API spec.
# Do not modify it by hand. Instead, modify the generator or the spec.
5 changes: 5 additions & 0 deletions api_generator/lib/_license_header.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
62 changes: 62 additions & 0 deletions api_generator/lib/components/action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.

# frozen_string_literal: true

require_relative 'argument'

# A collection of operations that comprise a single API Action
# AKA operation-group
class Action
# @return [Array<Operation>] Operations in the action
attr_reader :operations

# @param [Array<Operation>] operations
def initialize(operations)
@operations = operations
@operation = operations.first
@spec = @operation&.spec
end

# @return [Array<Argument>] Input arguments.
def arguments; @arguments ||= Argument.from_operations(@operations.map(&:spec)); end

# @return [Argument, NilClass] Request Body argument
def body; @body ||= arguments.find { |arg| arg.name == 'body' }; end

# @return [Array<Argument>] Path Param arguments
def path_params; @path_params ||= arguments.select { |arg| arg.location == 'path' }; end

# @return [Array<Argument>] Query Param arguments
def query_params; @query_params ||= arguments.select { |arg| arg.location == 'query' }; end

# @return [String] Full name of the action (i.e. x-operation-group)
def full_name; @operation&.group; end

# return [String] Name of the action
def name; @operation&.action; end

# @return [String] Namespace of the action
def namespace; @operation&.namespace; end

# @return [Array<String>] Sorted unique HTTP verbs
def http_verbs; @operations.map(&:http_verb).uniq.sort; end

# @return [Array<String>] Unique URLs
def urls; @operations.map(&:url).uniq; end

# @return [String] Description of the action
def description; @spec&.description || ''; end

# @return [Boolean] Whether the action is deprecated
def deprecated; @spec&.deprecated || false; end

# @return [String] Deprecation message
def deprecation_message; @spec['x-deprecation-message']; end

# @return [String, NilClass] API reference
def api_reference; @operation&.spec&.external_docs&.url; end
end
102 changes: 102 additions & 0 deletions api_generator/lib/components/argument.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# frozen_string_literal: true

# Represents an argument to an API action
class Argument
# @return [String] The name of the argument
attr_reader :name
# @return [String] The description of the argument
attr_reader :description
# @return [Boolean] Whether the argument is required
attr_reader :required
# @return [SpecHash] The JSON schema of the argument
attr_reader :schema
# @return [String] Argument type in Ruby
attr_reader :type
# @return [String] The default value of the argument
attr_reader :default
# @return [Boolean] Whether the argument is deprecated
attr_reader :deprecated
# @return [String] The deprecation message
attr_reader :deprecation_message
# @return [ArgLocation] The location of the argument
attr_reader :location

def initialize(name:, description:, required:, schema:, default:, deprecated:, deprecation_message:, location:)
@name = name
@description = description
@required = required
@schema = schema
@type = get_ruby_type(schema)
@default = default
@deprecated = deprecated
@deprecation_message = deprecation_message
@location = location
end

# @param [SpecHash | nil] schema
# @return [String | nil] Ruby type
def get_ruby_type(schema)
return nil if schema.nil?
union = schema.anyOf || schema.oneOf
return union.map { |sch| get_ruby_type(sch) }.join(', ') unless union.nil?
return 'Integer' if schema.type == 'integer'
return 'Float' if schema.type == 'number'
return 'Boolean' if schema.type == 'boolean'
return 'String' if schema.type == 'string'
return 'NilClass' if schema.type == 'null'
return "Array<#{get_ruby_type(schema.items)}>" if schema.type == 'array'
"Hash"
end

# @param [SpecHash] Full OpenAPI spec
def self.set_global(spec)
@global = spec.components.parameters.filter { |_, p| p['x-global'] }.map { |_, p| from_parameters([p], 1) }
end

# @return [Array<Argument>] Global arguments
def self.global
raise 'Global arguments not set' unless @global
@global
end

# @param [Array<SpecHash>] operations
# @return [Array<Argument>]
def self.from_operations(operations)
parameters = operations.flat_map(&:parameters).filter { |param| !param['x-global'] }
.group_by(&:name).values.map { |params| from_parameters(params, operations.size) }
body = from_request_bodies(operations.map(&:requestBody))
(parameters + [body]).compact
end

# @param [Array<SpecHash>] params
# @param [Integer] opts_count
# @return [Argument]
def self.from_parameters(params, opts_count)
param = params.first || SpecHash.new
schema = param&.schema || SpecHash.new
Argument.new(name: param.name,
description: param.description || schema.description,
required: params.filter(&:required).size >= opts_count,
schema: schema,
default: param.default || schema.default,
deprecated: param.deprecated || schema.deprecated,
deprecation_message: param['x-deprecation-message'] || schema['x-deprecation-message'],
location: params.any? { |p| p.in == 'path' } ? 'path' : 'query')
end

# @param [Array<SpecHash>] bodies
# @return [Argument | nil]
def self.from_request_bodies(bodies)
body = bodies.compact.find { |body| !body.nil? }
return if body.nil?
schema = body.content['application/json']&.schema || body.content['application/x-ndjson']&.schema || SpecHash.new
Argument.new(name: 'body',
description: body.description || schema.description,
required: bodies.all?(&:required),
schema: schema,
default: nil,
deprecated: body.deprecated || schema.deprecated,
deprecation_message: body['x-deprecation-message'] || schema&.[]('x-deprecation-message'),
location: 'body')
end
end
Loading

0 comments on commit c06bb81

Please sign in to comment.