Skip to content

Commit 46b0106

Browse files
author
Daniel Jackson
committed
Continued working on the Spect Insert feature for automatic conversion of API calls into Language SDK.
Signed-off-by: Daniel Jackson <[email protected]>
1 parent d428c57 commit 46b0106

File tree

6 files changed

+196
-244
lines changed

6 files changed

+196
-244
lines changed

_api-reference/cat/cat-allocation.md

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,6 @@ 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 -->
6430

6531
<!-- spec_insert_start
6632
api: cat.allocation

spec-insert/lib/insert_arguments.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
require_relative 'utils'
44
require_relative 'spec_insert_error'
5-
5+
require 'json'
66
# Doc Insert Arguments
77
class InsertArguments
88
attr_reader :raw
99

10+
Rest = Struct.new(:verb, :path, :query, :body, :raw_lines, keyword_init:true)
11+
1012
# @param [Hash] args raw arguments read from the doc insert marker
1113
def initialize(args)
1214
@raw = args.to_h.with_indifferent_access
@@ -61,6 +63,34 @@ def omit_header
6163
parse_boolean(@raw['omit_header'], default: false)
6264
end
6365

66+
# @return [Rest, nil]
67+
def rest
68+
lines = @raw['rest']&.split("\n")&.map(&:strip) || []
69+
return nil if lines.empty?
70+
71+
verb, full_path = lines.first.to_s.split
72+
path, query_string = full_path.to_s.split('?', 2)
73+
74+
query = (query_string || "").split('&').to_h do |pair|
75+
k, v = pair.split('=', 2)
76+
[k, v || "false"]
77+
end
78+
79+
body = begin
80+
JSON.parse(@raw['body']) if @raw['body']
81+
rescue JSON::ParserError
82+
@raw['body']
83+
end
84+
85+
Rest.new(
86+
verb: verb,
87+
path: path,
88+
query: query,
89+
body: body,
90+
raw_lines: lines
91+
)
92+
end
93+
6494
private
6595

6696
# @param [String] value comma-separated array
Lines changed: 10 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'json'
4+
require_relative 'example_code_python'
45

56
class ExampleCode < BaseMustacheRenderer
67
self.template_file = "#{__dir__}/templates/example_code.mustache"
@@ -9,217 +10,21 @@ def initialize(action, args)
910
super(action, args)
1011
end
1112

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-
2413
def rest_lines
25-
@args.raw['rest']&.split("\n")&.map(&:strip) || []
14+
@args.rest.raw_lines
2615
end
2716

2817
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
18+
base = rest_lines.join("\n")
19+
body = @args.rest.body
20+
if body
21+
body.is_a?(String) ? base + "\n" + body : base + "\n" + JSON.pretty_generate(body)
22+
else
23+
base
11524
end
116-
117-
@matching_spec_path = best
11825
end
11926

120-
# Final Python code using action metadata
12127
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
28+
ExampleCodePython.new(@action, @args).render
22429
end
225-
end
30+
end

0 commit comments

Comments
 (0)