Skip to content

Commit d428c57

Browse files
author
Daniel Jackson
committed
Updating Spect Insert tool to support client code translations
Signed-off-by: Daniel Jackson <[email protected]>
1 parent 09025be commit d428c57

File tree

7 files changed

+284
-4
lines changed

7 files changed

+284
-4
lines changed

_api-reference/cat/cat-allocation.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,40 @@ GET /_cat/allocation/{node_id}
2727
```
2828
<!-- spec_insert_end -->
2929

30+
<!-- spec_insert_start
31+
api: snapshot.restore
32+
component: example_code
33+
rest: GET _nodes/stats/indices
34+
-->
35+
{% capture step1_rest %}
36+
POST _snapshot/<repository>/<snapshot>/_restore
37+
{% endcapture %}
38+
39+
{% capture step1_python %}
40+
response = client.snapshot.restore(
41+
repository = "<repository>",
42+
snapshot = "<snapshot>",
43+
body = {
44+
"indices": "opendistro-reports-definitions",
45+
"ignore_unavailable": true,
46+
"include_global_state": false,
47+
"rename_pattern": "(.+)",
48+
"rename_replacement": "$1_restored",
49+
"include_aliases": false
50+
}
51+
)
52+
53+
{% endcapture %}
54+
55+
{% capture step1_javascript %}
56+
JavaScript example code not yet implemented
57+
{% endcapture %}
58+
59+
{% include code-block.html
60+
rest=step1_rest
61+
python=step1_python
62+
%}
63+
<!-- spec_insert_end -->
3064

3165
<!-- spec_insert_start
3266
api: cat.allocation

spec-insert/lib/insert_arguments.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def initialize(args)
1717
def self.from_marker(lines)
1818
end_index = lines.each_with_index.find { |line, _index| line.match?(/^\s*-->/) }&.last&.- 1
1919
args = lines[1..end_index].filter { |line| line.include?(':') }.to_h do |line|
20-
key, value = line.split(':')
20+
key, value = line.split(':',2)
2121
[key.strip, value.strip]
2222
end
2323
new(args)

spec-insert/lib/jekyll-spec-insert.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def self.process_file(file, fail_on_error: false)
3030
raise e if fail_on_error
3131
relative_path = Pathname(file).relative_path_from(Pathname.new(Dir.pwd))
3232
Jekyll.logger.error "Error processing #{relative_path}: #{e.message}"
33+
Jekyll.logger.error "Error backtrace: #{e.backtrace.join("\n")}"
3334
end
3435

3536
def self.watch(fail_on_error: false)
@@ -43,4 +44,4 @@ def self.watch(fail_on_error: false)
4344
trap('TERM') { exit }
4445
sleep
4546
end
46-
end
47+
end
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
class ExampleCode < BaseMustacheRenderer
6+
self.template_file = "#{__dir__}/templates/example_code.mustache"
7+
8+
def initialize(action, args)
9+
super(action, args)
10+
end
11+
12+
# Resolves the correct OpenSearch client method call
13+
def client_method_call
14+
segments = @action.full_name.to_s.split('.')
15+
return "client" if segments.empty?
16+
17+
if segments.size == 1
18+
"client.#{segments.first}"
19+
else
20+
"client.#{se gments.first}.#{segments[1]}"
21+
end
22+
end
23+
24+
def rest_lines
25+
@args.raw['rest']&.split("\n")&.map(&:strip) || []
26+
end
27+
28+
def rest_code
29+
rest_lines.join("\n")
30+
end
31+
32+
# Uses the declared HTTP method in the OpenAPI spec
33+
def http_method
34+
@action.http_verbs.first&.upcase || "GET"
35+
end
36+
37+
# Converts OpenAPI-style path (/index/{id}) into Ruby-style interpolation (/index/#{id})
38+
def path_only
39+
url = @action.urls.first
40+
return '' unless url
41+
url.gsub(/\{(\w+)\}/, '#{\1}')
42+
end
43+
def javascript_code
44+
"JavaScript example code not yet implemented"
45+
end
46+
# Assembles a query string from the declared query parameters
47+
def query_string
48+
return '' if @action.query_parameters.empty?
49+
@action.query_parameters.map { |param| "#{param.name}=example" }.join('&')
50+
end
51+
52+
# Combines path and query string for display
53+
def path_with_query
54+
qs = query_string
55+
qs.empty? ? path_only : "#{path_only}?#{qs}"
56+
end
57+
58+
# Hash version of query params
59+
def query_params
60+
@action.query_parameters.to_h { |param| [param.name, "example"] }
61+
end
62+
63+
# Parses the body from the REST example (only for preserving raw formatting)
64+
def body
65+
body_lines = rest_lines[1..]
66+
return nil if body_lines.empty?
67+
begin
68+
JSON.parse(body_lines.join("\n"))
69+
rescue
70+
nil
71+
end
72+
end
73+
74+
def action_expects_body?(verb)
75+
verb = verb.downcase
76+
@action.operations.any? do |op|
77+
op.http_verb.to_s.downcase == verb &&
78+
op.spec&.requestBody &&
79+
op.spec.requestBody.respond_to?(:content)
80+
end
81+
end
82+
83+
def matching_spec_path
84+
return @matching_spec_path if defined?(@matching_spec_path)
85+
86+
# Extract raw request path from rest line
87+
raw_line = rest_lines.first.to_s
88+
_, request_path = raw_line.split
89+
request_segments = request_path.split('?').first.split('/').reject(&:empty?)
90+
91+
# Choose the best matching spec URL
92+
best = nil
93+
best_score = -1
94+
95+
@action.urls.each do |spec_path|
96+
spec_segments = spec_path.split('/').reject(&:empty?)
97+
next unless spec_segments.size == request_segments.size
98+
99+
score = 0
100+
spec_segments.each_with_index do |seg, i|
101+
if seg.start_with?('{')
102+
score += 1 # parameter match
103+
elsif seg == request_segments[i]
104+
score += 2 # exact match
105+
else
106+
score = -1
107+
break
108+
end
109+
end
110+
111+
if score > best_score
112+
best = spec_path
113+
best_score = score
114+
end
115+
end
116+
117+
@matching_spec_path = best
118+
end
119+
120+
# Final Python code using action metadata
121+
def python_code
122+
return "# Invalid action" unless @action&.full_name
123+
124+
client_setup = <<~PYTHON
125+
from opensearchpy import OpenSearch
126+
127+
host = 'localhost'
128+
port = 9200
129+
auth = ('admin', 'admin') # For testing only. Don't store credentials in code.
130+
ca_certs_path = '/full/path/to/root-ca.pem' # Provide a CA bundle if you use intermediate CAs with your root CA.
131+
132+
# Create the client with SSL/TLS enabled, but hostname verification disabled.
133+
client = OpenSearch(
134+
hosts = [{'host': host, 'port': port}],
135+
http_compress = True, # enables gzip compression for request bodies
136+
http_auth = auth,
137+
use_ssl = True,
138+
verify_certs = True,
139+
ssl_assert_hostname = False,
140+
ssl_show_warn = False,
141+
ca_certs = ca_certs_path
142+
)
143+
144+
PYTHON
145+
146+
if @args.raw['body'] == '{"hello"}'
147+
puts "# This is a debug example"
148+
end
149+
150+
namespace, method = @action.full_name.split('.')
151+
client_call = "client"
152+
client_call += ".#{namespace}" if namespace
153+
client_call += ".#{method}"
154+
155+
args = []
156+
157+
# Extract actual path and query from the first line of the REST input
158+
raw_line = rest_lines.first.to_s
159+
http_verb, full_path = raw_line.split
160+
path_part, query_string = full_path.to_s.split('?', 2)
161+
162+
# Extract used path values from the path part
163+
path_values = path_part.split('/').reject(&:empty?)
164+
165+
# Match spec path (e.g. /_cat/aliases/{name}) to determine which param this value belongs to
166+
spec_path = matching_spec_path.to_s
167+
spec_parts = spec_path.split('/').reject(&:empty?)
168+
169+
param_mapping = {}
170+
spec_parts.each_with_index do |part, i|
171+
if part =~ /\{(.+?)\}/ && path_values[i]
172+
param_mapping[$1] = path_values[i]
173+
end
174+
end
175+
176+
# Add path parameters if they were present in the example
177+
@action.path_parameters.each do |param|
178+
if param_mapping.key?(param.name)
179+
args << "#{param.name} = \"#{param_mapping[param.name]}\""
180+
end
181+
end
182+
183+
# Add query parameters from query string
184+
if query_string
185+
query_pairs = query_string.split('&').map { |s| s.split('=', 2) }
186+
query_hash = query_pairs.map do |k, v|
187+
"#{k}: #{v ? "\"#{v}\"" : "True"}"
188+
end.join(', ')
189+
args << "params = { #{query_hash} }" unless query_hash.empty?
190+
end
191+
192+
# Add body if spec allows it AND it's present in REST
193+
if action_expects_body?(http_verb)
194+
if @args.raw['body']
195+
begin
196+
parsed = JSON.parse(@args.raw['body'])
197+
pretty = JSON.pretty_generate(parsed).gsub(/^/, ' ')
198+
args << "body = #{pretty}"
199+
rescue JSON::ParserError
200+
args << "body = #{JSON.dump(@args.raw['body'])}"
201+
end
202+
else
203+
args << 'body = { "Insert body here" }'
204+
end
205+
end
206+
207+
# Final result
208+
call_code = if args.empty?
209+
"response = #{client_call}()"
210+
else
211+
final_args = args.map { |line| " #{line}" }.join(",\n")
212+
<<~PYTHON
213+
response = #{client_call}(
214+
#{final_args}
215+
)
216+
PYTHON
217+
end
218+
# Prepend client if requested
219+
if @args.raw['include_client_setup']
220+
client_setup + call_code
221+
else
222+
call_code
223+
end
224+
end
225+
end

spec-insert/lib/renderers/spec_insert.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require_relative 'path_parameters'
88
require_relative 'query_parameters'
99
require_relative 'body_parameters'
10+
require_relative 'example_code'
1011

1112
# Class to render spec insertions
1213
class SpecInsert < BaseMustacheRenderer
@@ -40,8 +41,10 @@ def content
4041
BodyParameters.new(@action, @args, is_request: true).render
4142
when :response_body_parameters
4243
BodyParameters.new(@action, @args, is_request: false).render
44+
when :example_code
45+
ExampleCode.new(@action, @args).render
4346
else
44-
raise SpecInsertError, "Invalid component: #{@args.component}"
47+
raise SpecInsertError, "Invalid component: #{@args.component}, from spec_insert.rb "
4548
end
4649
end
4750
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% capture step1_rest %}
2+
{{{rest_code}}}
3+
{% endcapture %}
4+
5+
{% capture step1_python %}
6+
{{{python_code}}}
7+
{% endcapture %}
8+
9+
{% capture step1_javascript %}
10+
{{{javascript_code}}}
11+
{% endcapture %}
12+
13+
{% include code-block.html
14+
rest=step1_rest
15+
python=step1_python
16+
%}

spec-insert/lib/utils.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ module Utils
1313
'query_parameters' => 'Query Parameters',
1414
'path_parameters' => 'Path Parameters',
1515
'request_body_parameters' => 'Request Body Parameters',
16-
'response_body_parameters' => 'Response Body Parameters'
16+
'response_body_parameters' => 'Response Body Parameters',
17+
'example_code' => 'Example Code'
1718
}.freeze
1819

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

0 commit comments

Comments
 (0)