Skip to content

Commit acb7eb8

Browse files
author
Daniel Jackson
committed
Continued working on the Spect Insert feature for automatic conversion of API calls into Language SDK.
Refactored code base to seperate code logic and allow for more scalability in the future. Example Below: Since it might be asked,I will say we did try to make the rest and body tag one line, and we tried many solutions. but came to no avail for a working solution. Will continue to work on this to try to find a solution. <!-- spec_insert_start api: index component: example_code rest: PUT /my-index/_doc/1 body: {"title":"Spec Insert Test","timestamp":"2025-07-09T22:00:00Z"} --> {% capture step1_rest %} PUT /my-index/_doc/1 { "title": "Spec Insert Test", "timestamp": "2025-07-09T22:00:00Z" } {% endcapture %} {% capture step1_python %} response = client.index( index = "my-index", id = "1", body = { "title": "Spec Insert Test", "timestamp": "2025-07-09T22:00:00Z" } ) {% endcapture %} {% include code-block.html rest=step1_rest python=step1_python %} <!-- spec_insert_end --> Signed-off-by: Daniel Jackson <[email protected]>
1 parent 9cb86b0 commit acb7eb8

File tree

9 files changed

+199
-247
lines changed

9 files changed

+199
-247
lines changed

_api-reference/cat/cat-aliases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The following table lists the available query parameters. All query parameters a
4141

4242
| Parameter | Data type | Description | Default |
4343
| :--- | :--- | :--- | :--- |
44-
| `expand_wildcards` | List or String | Specifies the type of index that wildcard expressions can match. Supports comma-separated values. <br> Valid values are: <br> - `all`: Match any index, including hidden ones. <br> - `closed`: Match closed, non-hidden indexes. <br> - `hidden`: Match hidden indexes. Must be combined with open, closed, or both. <br> - `none`: Wildcard expressions are not accepted. <br> - `open`: Match open, non-hidden indexes. | N/A |
44+
| `expand_wildcards` | List or String | Specifies the type of index that wildcard expressions can match. Supports comma-separated values. <br> Valid values are: <br> - `all`: Match any index, including hidden ones. <br> - `closed`: Match closed, non-hidden indexes. <br> - `hidden`: Match hidden indexes. Must be combined with `open`, `closed`, or both. <br> - `none`: Wildcard expressions are not accepted. <br> - `open`: Match open, non-hidden indexes. | N/A |
4545
| `format` | String | A short version of the `Accept` header, such as `json` or `yaml`. | N/A |
4646
| `h` | List | A comma-separated list of column names to display. | N/A |
4747
| `help` | Boolean | Returns help information. | `false` |

_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

_api-reference/cat/cat-indices.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The following table lists the available query parameters. All query parameters a
4141
| :--- | :--- | :--- | :--- |
4242
| `bytes` | String | The units used to display byte values. <br> Valid values are: `b`, `kb`, `k`, `mb`, `m`, `gb`, `g`, `tb`, `t`, `pb`, and `p`. | N/A |
4343
| `cluster_manager_timeout` | String | The amount of time allowed to establish a connection to the cluster manager node. | N/A |
44-
| `expand_wildcards` | List or String | Specifies the type of index that wildcard expressions can match. Supports comma-separated values. <br> Valid values are: <br> - `all`: Match any index, including hidden ones. <br> - `closed`: Match closed, non-hidden indexes. <br> - `hidden`: Match hidden indexes. Must be combined with open, closed, or both. <br> - `none`: Wildcard expressions are not accepted. <br> - `open`: Match open, non-hidden indexes. | N/A |
44+
| `expand_wildcards` | List or String | Specifies the type of index that wildcard expressions can match. Supports comma-separated values. <br> Valid values are: <br> - `all`: Match any index, including hidden ones. <br> - `closed`: Match closed, non-hidden indexes. <br> - `hidden`: Match hidden indexes. Must be combined with `open`, `closed`, or both. <br> - `none`: Wildcard expressions are not accepted. <br> - `open`: Match open, non-hidden indexes. | N/A |
4545
| `format` | String | A short version of the `Accept` header, such as `json` or `yaml`. | N/A |
4646
| `h` | List | A comma-separated list of column names to display. | N/A |
4747
| `health` | String | Limits indexes based on their health status. Supported values are `green`, `yellow`, and `red`. <br> Valid values are: `green`, `GREEN`, `yellow`, `YELLOW`, `red`, and `RED`. | N/A |

_api-reference/cat/cat-segment-replication.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The following table lists the available query parameters. All query parameters a
6060
| `bytes` | String | The units used to display byte values. <br> Valid values are: `b`, `kb`, `k`, `mb`, `m`, `gb`, `g`, `tb`, `t`, `pb`, and `p`. | N/A |
6161
| `completed_only` | Boolean | When `true`, the response only includes the last-completed segment replication events. | `false` |
6262
| `detailed` | Boolean | When `true`, the response includes additional metrics for each stage of a segment replication event. | `false` |
63-
| `expand_wildcards` | List or String | Specifies the type of index that wildcard expressions can match. Supports comma-separated values. <br> Valid values are: <br> - `all`: Match any index, including hidden ones. <br> - `closed`: Match closed, non-hidden indexes. <br> - `hidden`: Match hidden indexes. Must be combined with open, closed, or both. <br> - `none`: Wildcard expressions are not accepted. <br> - `open`: Match open, non-hidden indexes. | N/A |
63+
| `expand_wildcards` | List or String | Specifies the type of index that wildcard expressions can match. Supports comma-separated values. <br> Valid values are: <br> - `all`: Match any index, including hidden ones. <br> - `closed`: Match closed, non-hidden indexes. <br> - `hidden`: Match hidden indexes. Must be combined with `open`, `closed`, or both. <br> - `none`: Wildcard expressions are not accepted. <br> - `open`: Match open, non-hidden indexes. | N/A |
6464
| `format` | String | A short version of the `Accept` header, such as `json` or `yaml`. | N/A |
6565
| `h` | List | A comma-separated list of column names to display. | N/A |
6666
| `help` | Boolean | Returns help information. | `false` |

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)