Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions spec-insert/lib/api/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ def deprecated; @spec.deprecated; end
# @return [String] Deprecation message
def deprecation_message; @spec['x-deprecation-message']; end

def self.find_by_rest(rest_line)
method, raw_path = rest_line.strip.split(' ', 2)
return nil unless method && raw_path

# Remove query parameters
path = raw_path.split('?').first

all.find do |action|
action.operations.any? do |op|
op.http_verb.casecmp?(method) &&
path_template_matches?(op.url, path)
end
end
end

def self.path_template_matches?(template, actual)
# "/{index}/_doc/{id}" => "^/[^/]+/_doc/[^/]+$"
regex = Regexp.new("^" + template.gsub(/\{[^\/]+\}/, '[^/]+') + "$")
regex.match?(actual)
end
# @return [String] API reference
def api_reference; @operation.external_docs.url; end
end
Expand Down
45 changes: 42 additions & 3 deletions spec-insert/lib/insert_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

require_relative 'utils'
require_relative 'spec_insert_error'

require 'json'
# Doc Insert Arguments
class InsertArguments
attr_reader :raw

Rest = Struct.new(:verb, :path, :query, :body, :raw_lines, keyword_init:true)

# @param [Hash] args raw arguments read from the doc insert marker
def initialize(args)
@raw = args.to_h.with_indifferent_access
Expand All @@ -17,15 +19,24 @@ def initialize(args)
def self.from_marker(lines)
end_index = lines.each_with_index.find { |line, _index| line.match?(/^\s*-->/) }&.last&.- 1
args = lines[1..end_index].filter { |line| line.include?(':') }.to_h do |line|
key, value = line.split(':')
key, value = line.split(':',2)
[key.strip, value.strip]
end
new(args)
end

# @return [String]
def api
@raw['api']
return @raw['api'] if @raw['api'].present?

if rest_line = rest&.raw_lines&.first
inferred_action = Api::Action.find_by_rest(rest_line)
raise SpecInsertError, "Could not infer API from rest line: #{rest_line}" unless inferred_action

return inferred_action.full_name
end

nil
end

# @return [String]
Expand Down Expand Up @@ -61,6 +72,34 @@ def omit_header
parse_boolean(@raw['omit_header'], default: false)
end

# @return [Rest, nil]
def rest
lines = @raw['rest']&.split("\n")&.map(&:strip) || []
return nil if lines.empty?

verb, full_path = lines.first.to_s.split
path, query_string = full_path.to_s.split('?', 2)

query = (query_string || "").split('&').to_h do |pair|
k, v = pair.split('=', 2)
[k, v || "false"]
end

body = begin
JSON.parse(@raw['body']) if @raw['body']
rescue JSON::ParserError
@raw['body']
end

Rest.new(
verb: verb,
path: path,
query: query,
body: body,
raw_lines: lines
)
end

private

# @param [String] value comma-separated array
Expand Down
3 changes: 2 additions & 1 deletion spec-insert/lib/jekyll-spec-insert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def self.process_file(file, fail_on_error: false)
raise e if fail_on_error
relative_path = Pathname(file).relative_path_from(Pathname.new(Dir.pwd))
Jekyll.logger.error "Error processing #{relative_path}: #{e.message}"
Jekyll.logger.error "Error backtrace: #{e.backtrace.join("\n")}"
end

def self.watch(fail_on_error: false)
Expand All @@ -43,4 +44,4 @@ def self.watch(fail_on_error: false)
trap('TERM') { exit }
sleep
end
end
end
30 changes: 30 additions & 0 deletions spec-insert/lib/renderers/example_code.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require 'json'
require_relative 'example_code_python'

class ExampleCode < BaseMustacheRenderer
self.template_file = "#{__dir__}/templates/example_code.mustache"

def initialize(action, args)
super(action, args)
end

def rest_lines
@args.rest.raw_lines
end

def rest_code
base = rest_lines.join("\n")
body = @args.rest.body
if body
body.is_a?(String) ? base + "\n" + body : base + "\n" + JSON.pretty_generate(body)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the condition here necessary? Can you put JSON.pretty_generate in a try/catch to guard against exceptions?

else
base
end
end

def python_code
ExampleCodePython.new(@action, @args).render
end
end
152 changes: 152 additions & 0 deletions spec-insert/lib/renderers/example_code_python.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
require 'json'

class ExampleCodePython < BaseMustacheRenderer
self.template_file = "#{__dir__}/templates/example_code.python.mustache"

def initialize(action, args)
super(action, args)
end

def call_code
return "# Invalid action" unless @action&.full_name
client_setup = <<~PYTHON
from opensearchpy import OpenSearch

host = 'localhost'
port = 9200
auth = ('admin', 'admin') # For testing only. Don't store credentials in code.
ca_certs_path = '/full/path/to/root-ca.pem' # Provide a CA bundle if you use intermediate CAs with your root CA.

# Create the client with SSL/TLS enabled, but hostname verification disabled.
client = OpenSearch(
hosts = [{'host': host, 'port': port}],
http_compress = True, # enables gzip compression for request bodies
http_auth = auth,
use_ssl = True,
verify_certs = True,
ssl_assert_hostname = False,
ssl_show_warn = False,
ca_certs = ca_certs_path
)

PYTHON

parts = @action.full_name.split('.')
client_call = "client"

if parts.length == 2
namespace, method = parts
client_call += ".#{namespace}.#{method}"
else
namespace = parts[0]
client_call += ".#{namespace}"
end

args = []

rest = @args.rest
http_verb = rest.verb
full_path = [rest.path, rest.query&.map { |k,v| "#{k}=#{v}" }.join('&')].compact.join('?')
path_part, query_string = full_path.to_s.split('?', 2)
path_values = path_part.split('/').reject(&:empty?)

spec_path = match_spec_path(full_path)
spec_parts = spec_path.split('/').reject(&:empty?)

param_mapping = {}
spec_parts.each_with_index do |part, i|
if part =~ /\{(.+?)\}/ && path_values[i]
param_mapping[$1] = path_values[i]
end
end

@action.path_parameters.each do |param|
if param_mapping.key?(param.name)
args << "#{param.name} = \"#{param_mapping[param.name]}\""
end
end

if query_string
query_pairs = query_string.split('&').map { |s| s.split('=', 2) }
query_hash = query_pairs.map do |k, v|
"\"#{k}\": #{v ? "\"#{v}\"" : "\"false\""}"
end.join(', ')
args << "params = { #{query_hash} }" unless query_hash.empty?
end

body = rest.body
if expects_body?(http_verb)
if body
begin
parsed = JSON.parse(@args.raw['body'])
pretty = JSON.pretty_generate(parsed).gsub(/^/, ' ')
args << "body = #{pretty}"
rescue JSON::ParserError
args << "body = #{JSON.dump(@args.raw['body'])}"
end
else
args << 'body = { "Insert body here" }'
end
end

python_setup = if args.empty?
"response = #{client_call}()"
else
final_args = args.map { |line| " #{line}" }.join(",\n")
<<~PYTHON

response = #{client_call}(
#{final_args}
)
PYTHON
end
if @args.raw['include_client_setup']
client_setup + python_setup
else
python_setup
end
end

private

def expects_body?(verb)
verb = verb.downcase
@action.operations.any? do |op|
op.http_verb.to_s.downcase == verb &&
op.spec&.requestBody &&
op.spec.requestBody.respond_to?(:content)
end
end

def match_spec_path(full_path)
request_path = full_path.split('?').first
request_segments = request_path.split('/').reject(&:empty?)

best = ''
best_score = -1

@action.urls.each do |spec_path|
spec_segments = spec_path.split('/').reject(&:empty?)
next unless spec_segments.size == request_segments.size

score = 0
spec_segments.each_with_index do |seg, i|
if seg.start_with?('{')
score += 1
elsif seg == request_segments[i]
score += 2
else
score = -1
break
end
end

if score > best_score
best = spec_path
best_score = score
end
end

best
end
end
7 changes: 5 additions & 2 deletions spec-insert/lib/renderers/spec_insert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require_relative 'path_parameters'
require_relative 'query_parameters'
require_relative 'body_parameters'
require_relative 'example_code'

# Class to render spec insertions
class SpecInsert < BaseMustacheRenderer
Expand All @@ -16,7 +17,7 @@ class SpecInsert < BaseMustacheRenderer
def initialize(args)
action = Api::Action.by_full_name[args.api]
super(action, args)
raise SpecInsertError, '`api` argument not specified.' unless @args.api
raise SpecInsertError, '`api` argument could not be resolved.' unless @action
raise SpecInsertError, "API Action '#{@args.api}' does not exist in the spec." unless @action
end

Expand All @@ -40,8 +41,10 @@ def content
BodyParameters.new(@action, @args, is_request: true).render
when :response_body_parameters
BodyParameters.new(@action, @args, is_request: false).render
when :example_code
ExampleCode.new(@action, @args).render
else
raise SpecInsertError, "Invalid component: #{@args.component}"
raise SpecInsertError, "Invalid component: #{@args.component}, from spec_insert.rb "
end
end
end
11 changes: 11 additions & 0 deletions spec-insert/lib/renderers/templates/example_code.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% capture step1_rest %}
{{{rest_code}}}
{% endcapture %}

{% capture step1_python %}
{{{python_code}}}
{% endcapture %}

{% include code-block.html
rest=step1_rest
python=step1_python %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{! example_code.python.mustache }}

{{{call_code}}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template logic in example_code_python.rb should be put into the mustache template file to help better visualize what the end result would look like. Else we get a 2-line template like this which is not useful.

Not a blocker. You should resolve this in a follow up.

3 changes: 2 additions & 1 deletion spec-insert/lib/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ module Utils
'query_parameters' => 'Query Parameters',
'path_parameters' => 'Path Parameters',
'request_body_parameters' => 'Request Body Parameters',
'response_body_parameters' => 'Response Body Parameters'
'response_body_parameters' => 'Response Body Parameters',
'example_code' => 'Example Code'
}.freeze

# @return [Array<String>] list of markdown files to insert the spec components into
Expand Down