Skip to content

Commit

Permalink
Spec Insert
Browse files Browse the repository at this point in the history
A program that insert API Components generated from the OpenSearch OpenAPI Spec into markdown files

Signed-off-by: Theo Truong <[email protected]>
  • Loading branch information
nhtruong committed Oct 7, 2024
1 parent 59bea71 commit 8234d31
Show file tree
Hide file tree
Showing 26 changed files with 1,247 additions and 0 deletions.
2 changes: 2 additions & 0 deletions spec_insert/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
opensearch-openapi.yaml
rspec_examples.txt
1 change: 1 addition & 0 deletions spec_insert/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
25 changes: 25 additions & 0 deletions spec_insert/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require: rubocop-rake
AllCops:
Include:
- 'lib/**/*.rb'
- 'Rakefile'
NewCops: enable

Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/ParameterLists:
Enabled: false
Metrics/AbcSize:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false

Layout/EmptyLineAfterGuardClause:
Enabled: false

Style/MultilineBlockChain:
Enabled: false
Style/SingleLineMethods:
Enabled: false
1 change: 1 addition & 0 deletions spec_insert/.ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.1.0
91 changes: 91 additions & 0 deletions spec_insert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# README: Spec Insert

Check failure on line 1 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [OpenSearch.HeadingCapitalization] 'README: Spec Insert' is a heading and should be in sentence case. Raw Output: {"message": "[OpenSearch.HeadingCapitalization] 'README: Spec Insert' is a heading and should be in sentence case.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 1, "column": 3}}}, "severity": "ERROR"}
- [What is this?](#what-is-this)
- [Installation](#Installation)
- [How to use](#how-to-use)
- [Insert Query Parameters](#insert-query-parameters)
- [Insert Path Parameters](#insert-path-parameters)
- [Insert Paths and HTTP Methods](#insert-paths-and-http-methods)
- [Ignored files and folders](#ignored-files-and-folders)

## What is this?

Check failure on line 10 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [OpenSearch.HeadingPunctuation] Don't use punctuation at the end of a heading. Raw Output: {"message": "[OpenSearch.HeadingPunctuation] Don't use punctuation at the end of a heading.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 10, "column": 16}}}, "severity": "ERROR"}
This program allows you to insert API components generated from the OpenSearch Specification into this repository's markdown files. It's still underdevelopment, and many features are not yet implemented. This document will be updated as the program evolves.

## Installation
1. Clone this repository.
2. Change to the `spec_insert` directory.
3. Install Ruby 3.1.0 or later.
4. Install the required gems by running `bundle install`.

## How to use
Edit your markdown file and insert the following snippet where you want the API components to be inserted:
```markdown
<!-- spec_insert_start
api: <API_NAME>
component: <COMPONENT_NAME>
other_param: <OTHER_PARAM>
-->

This is where the API component will be inserted.
Everything between the `spec_insert_start` and `spec_insert_end` tags will be overwritten.

<!-- spec_insert_end -->
```

Then run the following Rake commands to download the latest OpenSearch Specification and insert the API components into the markdown files:
```shell
rake download_spec
rake insert_spec
```

### Insert Query Parameters

Check failure on line 40 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [OpenSearch.HeadingCapitalization] 'Insert Query Parameters' is a heading and should be in sentence case. Raw Output: {"message": "[OpenSearch.HeadingCapitalization] 'Insert Query Parameters' is a heading and should be in sentence case.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 40, "column": 5}}}, "severity": "ERROR"}
To insert query parameters table of the `cat.indices` API, use the following snippet:
```markdown
<!-- spec_insert_start
api: cat.indices
component: query_parameters
-->
<!-- spec_insert_end -->
```

- This will insert the query parameters of the `cat.indices` API into the markdown file 3 default columns: `Parameter`, `Type`, and `Description`. There are 5 columns that can be inserted: `Parameter`, `Type`, `Description`, `Required`, and `Default`. When `Required`/`Default` is not chosen, the info will be written in the `Description` column.
- This component accepts `include_global` (boolean, default to `false`) argument to include global query parameters in the table.

Check failure on line 51 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'Boolean' instead of 'boolean'. Raw Output: {"message": "[Vale.Terms] Use 'Boolean' instead of 'boolean'.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 51, "column": 44}}}, "severity": "ERROR"}
- This component accepts `include_deprecated` (boolean, default to `true`) argument to include deprecated parameters in the table.

Check failure on line 52 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'Boolean' instead of 'boolean'. Raw Output: {"message": "[Vale.Terms] Use 'Boolean' instead of 'boolean'.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 52, "column": 48}}}, "severity": "ERROR"}
- This component accepts `pretty` (boolean, default to `false`) argument to render the table in the pretty format instead of the compact format.

Check failure on line 53 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'Boolean' instead of 'boolean'. Raw Output: {"message": "[Vale.Terms] Use 'Boolean' instead of 'boolean'.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 53, "column": 36}}}, "severity": "ERROR"}

```markdown
<!-- spec_insert_start
api: cat.indices
component: query_parameters
include_global: true
include_deprecated: false
pretty: true
-->
<!-- spec_insert_end -->
```

### Insert Path Parameters

Check failure on line 66 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [OpenSearch.HeadingCapitalization] 'Insert Path Parameters' is a heading and should be in sentence case. Raw Output: {"message": "[OpenSearch.HeadingCapitalization] 'Insert Path Parameters' is a heading and should be in sentence case.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 66, "column": 5}}}, "severity": "ERROR"}

To insert path parameters table of the `indices.create` API, use the following snippet:
```markdown
<!-- spec_insert_start
api: indices.create
component: path_parameters
-->
<!-- spec_insert_end -->
```

This table behaves the same as the query parameters table except that it does not accept the `include_global` argument.

### Insert Paths and HTTP Methods

Check failure on line 79 in spec_insert/README.md

View workflow job for this annotation

GitHub Actions / style-job

[vale] reported by reviewdog 🐶 [OpenSearch.HeadingCapitalization] 'Insert Paths and HTTP Methods' is a heading and should be in sentence case. Raw Output: {"message": "[OpenSearch.HeadingCapitalization] 'Insert Paths and HTTP Methods' is a heading and should be in sentence case.", "location": {"path": "spec_insert/README.md", "range": {"start": {"line": 79, "column": 5}}}, "severity": "ERROR"}

To insert paths and HTTP methods of the `search` API, use the following snippet:
```markdown
<!-- spec_insert_start
api: search
component: paths_and_http_methods
-->
<!-- spec_insert_end -->
```

### Ignored files and folders
The program will ignore all markdown files whose names are in ALL CAPS. On top of that, you can also add files and folders you want to the [ignored.txt](./ignored.txt) file. Each line in the file should be the name of a file or folder you want to ignore.
23 changes: 23 additions & 0 deletions spec_insert/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require 'rake'

desc 'Download the OpenSearch API specification'
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

desc 'Insert the OpenSearch API specification info into the documentation'
task :insert_spec do
require_relative 'lib/spec_inserter'
require_relative 'lib/doc_processor'

SpecInserter.new(
root_folder: '../',
spec_file: './opensearch-openapi.yaml',
ignored: './ignored.txt'
).insert_spec
end
19 changes: 19 additions & 0 deletions spec_insert/gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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', '~> 13'
gem 'activesupport', '~> 7'
gem 'mustache', '~> 1'

group :development, :test do
gem 'rspec'
gem 'rubocop', '~> 1.44', require: false
gem 'rubocop-rake', require: false
end
5 changes: 5 additions & 0 deletions spec_insert/ignored.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# All files and folders listed below are ignored by the spec_insert program.
# MD files whose names are in ALL CAPS are always ignored.

spec_insert/
release-notes/
68 changes: 68 additions & 0 deletions spec_insert/lib/components/action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 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 'parameter'
require_relative 'operation'

# A collection of operations that comprise a single API Action
# AKA operation-group
class Action
# @param [SpecHash] spec Parsed OpenAPI spec
def self.actions=(spec)
operations = spec.paths.flat_map do |url, ops|
ops.filter_map { |verb, op| Operation.new(op, url, verb) unless op['x-ignorable'] }
end
@actions = operations.group_by(&:group).values.map { |ops| Action.new(ops) }.index_by(&:full_name)
end

# @return [Hash<String, Action>] API Actions indexed by operation-group
def self.actions
raise 'Actions not set' unless @actions
@actions
end

# @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<Parameter>] Input arguments.
def arguments; @arguments ||= Parameter.from_operations(@operations.map(&:spec)); end

# @return [String] Full name of the action (i.e. namespace.action)
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; end

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

# @return [String] API reference
def api_reference; @operation&.external_docs&.url; end
end
34 changes: 34 additions & 0 deletions spec_insert/lib/components/operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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

# An API Operation
class Operation
# @return [Openapi3Parser::Node::Operation] Operation Spec
attr_reader :spec
# @return [String] URL
attr_reader :url
# @return [String] HTTP Verb
attr_reader :http_verb
# @return [String] Operation Group
attr_reader :group
# @return [String] API Action
attr_reader :action
# @return [String] API Namespace
attr_reader :namespace

# @param [Openapi3Parser::Node::Operation] spec Operation Spec
# @param [String] url
# @param [String] http_verb
def initialize(spec, url, http_verb)
@spec = spec
@url = url
@http_verb = http_verb.upcase
@group = spec['x-operation-group']
@action, @namespace = @group.split('.').reverse
end
end
94 changes: 94 additions & 0 deletions spec_insert/lib/components/parameter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

module ArgLocation
PATH = :path
QUERY = :query
end

# Represents a parameter of an API action
class Parameter
# @return [String] The name of the parameter
attr_reader :name
# @return [String] The description of the parameter
attr_reader :description
# @return [Boolean] Whether the parameter is required
attr_reader :required
# @return [SpecHash] The JSON schema of the parameter
attr_reader :schema
# @return [String] Argument type in documentation
attr_reader :doc_type
# @return [String] The default value of the parameter
attr_reader :default
# @return [Boolean] Whether the parameter is deprecated
attr_reader :deprecated
# @return [String] The deprecation message
attr_reader :deprecation_message
# @return [String] The OpenSearch version when the parameter was deprecated
attr_reader :version_deprecated
# @return [ArgLocation] The location of the parameter
attr_reader :location

def initialize(name:, description:, required:, schema:, default:, deprecated:, deprecation_message:,
version_deprecated:, location:)
@name = name
@description = description
@required = required
@schema = schema
@doc_type = get_doc_type(schema).gsub('String / List', 'List').gsub('List / String', 'List')
@default = default
@deprecated = deprecated
@deprecation_message = deprecation_message
@version_deprecated = version_deprecated
@location = location
end

# @param [SpecHash | nil] schema
# @return [String | nil] Documentation type
def get_doc_type(schema)
return nil if schema.nil?
union = schema.anyOf || schema.oneOf
return union.map { |sch| get_doc_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 'NULL' if schema.type == 'null'
return 'List' if schema.type == 'array'
'Object'
end

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

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

# @param [Array<SpecHash>] operations List of operations of the same group
# @return [Array<Parameter>] List of parameters of the operation group
def self.from_operations(operations)
operations.flat_map(&:parameters).filter { |param| !param['x-global'] }
.group_by(&:name).values.map { |params| from_parameters(params, operations.size) }
end

# @param [Array<SpecHash>] params List of parameters of the same name
# @param [Integer] opts_count Number of operations involved
# @return [Parameter] Single parameter distilled from the list
def self.from_parameters(params, opts_count)
param = params.first || SpecHash.new
schema = param&.schema || SpecHash.new
Parameter.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'],
version_deprecated: param['x-version-deprecated'] || schema['x-version-deprecated'],
location: params.any? { |p| p.in == 'path' } ? ArgLocation::PATH : ArgLocation::QUERY)
end
end
Loading

0 comments on commit 8234d31

Please sign in to comment.