From cc6606bee3742ba25ed0b15788750eae2e81e050 Mon Sep 17 00:00:00 2001 From: Daniel Jackson Date: Tue, 17 Jun 2025 15:30:20 -0700 Subject: [PATCH 1/2] Started working on the Spect Insert feature for automatic conversion of API calls into Language SDK. Started with python first - Added "Example_code in utils.rb" - Added Switch statement for example_code - implemented example_code.rb to hande logic for each language - example_code.mustache for templating - cat-allocation.md will be the starting test file --- _api-reference/cat/cat-allocation.md | 23 ++++++++++++++ spec-insert/lib/renderers/example_code.rb | 31 +++++++++++++++++++ spec-insert/lib/renderers/spec_insert.rb | 5 ++- .../renderers/templates/example_code.mustache | 17 ++++++++++ spec-insert/lib/utils.rb | 3 +- 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 spec-insert/lib/renderers/example_code.rb create mode 100644 spec-insert/lib/renderers/templates/example_code.mustache diff --git a/_api-reference/cat/cat-allocation.md b/_api-reference/cat/cat-allocation.md index f0e2859680d..6d8c801ddb3 100644 --- a/_api-reference/cat/cat-allocation.md +++ b/_api-reference/cat/cat-allocation.md @@ -27,6 +27,29 @@ GET /_cat/allocation/{node_id} ``` + +{% capture step1_rest %} +GET /_cat/allocation?v +{% endcapture %} + +{% capture step1_python %} +response = client.cat.allocation(v: true) +{% endcapture %} + +{% capture step1_javascript %} +// TODO: add JS client call for cat.allocation +{% endcapture %} + +{% include code-block.html + rest=step1_rest + python=step1_python + javascript=step1_javascript +%} + {% capture step1_rest %} -GET /_cat/allocation?v +POST _snapshot///_restore {% endcapture %} {% capture step1_python %} -response = client.cat.allocation(v: true) +response = client.snapshot.restore( + repository = "", + snapshot = "", + body = { + "indices": "opendistro-reports-definitions", + "ignore_unavailable": true, + "include_global_state": false, + "rename_pattern": "(.+)", + "rename_replacement": "$1_restored", + "include_aliases": false + } +) + {% endcapture %} {% capture step1_javascript %} -// TODO: add JS client call for cat.allocation +JavaScript example code not yet implemented {% endcapture %} {% include code-block.html - rest=step1_rest - python=step1_python - javascript=step1_javascript +rest=step1_rest +python=step1_python %} diff --git a/spec-insert/lib/insert_arguments.rb b/spec-insert/lib/insert_arguments.rb index aa05cc01df1..beb515d617d 100644 --- a/spec-insert/lib/insert_arguments.rb +++ b/spec-insert/lib/insert_arguments.rb @@ -17,7 +17,7 @@ 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) diff --git a/spec-insert/lib/jekyll-spec-insert.rb b/spec-insert/lib/jekyll-spec-insert.rb index f474fdd1235..1ac8d4ea884 100644 --- a/spec-insert/lib/jekyll-spec-insert.rb +++ b/spec-insert/lib/jekyll-spec-insert.rb @@ -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) @@ -43,4 +44,4 @@ def self.watch(fail_on_error: false) trap('TERM') { exit } sleep end -end +end \ No newline at end of file diff --git a/spec-insert/lib/renderers/example_code.rb b/spec-insert/lib/renderers/example_code.rb index d1cd61f4f92..25c62195dbe 100644 --- a/spec-insert/lib/renderers/example_code.rb +++ b/spec-insert/lib/renderers/example_code.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json' + class ExampleCode < BaseMustacheRenderer self.template_file = "#{__dir__}/templates/example_code.mustache" @@ -7,25 +9,217 @@ def initialize(action, args) super(action, args) end - def query_params - @args.raw['query_params']&.split(',')&.map(&:strip) || [] + # Resolves the correct OpenSearch client method call + def client_method_call + segments = @action.full_name.to_s.split('.') + return "client" if segments.empty? + + if segments.size == 1 + "client.#{segments.first}" + else + "client.#{se gments.first}.#{segments[1]}" + end + end + + def rest_lines + @args.raw['rest']&.split("\n")&.map(&:strip) || [] end def rest_code - method = @action.http_verbs.first.upcase rescue 'GET' - path = @action.urls.first rescue '/' - query = query_params - query_string = query.any? ? "?#{query.join('&')}" : "" - "#{method} #{path}#{query_string}" + rest_lines.join("\n") end - def python_code - query = query_params - args = query.map { |k| "#{k}: true" }.join(', ') - "response = client.#{@action.full_name}(#{args})" + # Uses the declared HTTP method in the OpenAPI spec + def http_method + @action.http_verbs.first&.upcase || "GET" end + # Converts OpenAPI-style path (/index/{id}) into Ruby-style interpolation (/index/#{id}) + def path_only + url = @action.urls.first + return '' unless url + url.gsub(/\{(\w+)\}/, '#{\1}') + end def javascript_code - "// TODO: add JS client call for #{@action.full_name}" + "JavaScript example code not yet implemented" + end + # Assembles a query string from the declared query parameters + def query_string + return '' if @action.query_parameters.empty? + @action.query_parameters.map { |param| "#{param.name}=example" }.join('&') + end + + # Combines path and query string for display + def path_with_query + qs = query_string + qs.empty? ? path_only : "#{path_only}?#{qs}" + end + + # Hash version of query params + def query_params + @action.query_parameters.to_h { |param| [param.name, "example"] } + end + + # Parses the body from the REST example (only for preserving raw formatting) + def body + body_lines = rest_lines[1..] + return nil if body_lines.empty? + begin + JSON.parse(body_lines.join("\n")) + rescue + nil + end + end + + def action_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 matching_spec_path + return @matching_spec_path if defined?(@matching_spec_path) + + # Extract raw request path from rest line + raw_line = rest_lines.first.to_s + _, request_path = raw_line.split + request_segments = request_path.split('?').first.split('/').reject(&:empty?) + + # Choose the best matching spec URL + best = nil + 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 # parameter match + elsif seg == request_segments[i] + score += 2 # exact match + else + score = -1 + break + end + end + + if score > best_score + best = spec_path + best_score = score + end + end + + @matching_spec_path = best + end + + # Final Python code using action metadata + def python_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 + + if @args.raw['body'] == '{"hello"}' + puts "# This is a debug example" + end + + namespace, method = @action.full_name.split('.') + client_call = "client" + client_call += ".#{namespace}" if namespace + client_call += ".#{method}" + + args = [] + + # Extract actual path and query from the first line of the REST input + raw_line = rest_lines.first.to_s + http_verb, full_path = raw_line.split + path_part, query_string = full_path.to_s.split('?', 2) + + # Extract used path values from the path part + path_values = path_part.split('/').reject(&:empty?) + + # Match spec path (e.g. /_cat/aliases/{name}) to determine which param this value belongs to + spec_path = matching_spec_path.to_s + 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 + + # Add path parameters if they were present in the example + @action.path_parameters.each do |param| + if param_mapping.key?(param.name) + args << "#{param.name} = \"#{param_mapping[param.name]}\"" + end + end + + # Add query parameters from query string + if query_string + query_pairs = query_string.split('&').map { |s| s.split('=', 2) } + query_hash = query_pairs.map do |k, v| + "#{k}: #{v ? "\"#{v}\"" : "True"}" + end.join(', ') + args << "params = { #{query_hash} }" unless query_hash.empty? + end + + # Add body if spec allows it AND it's present in REST + if action_expects_body?(http_verb) + if @args.raw['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 + + # Final result + call_code = if args.empty? + "response = #{client_call}()" + else + final_args = args.map { |line| " #{line}" }.join(",\n") + <<~PYTHON + response = #{client_call}( + #{final_args} + ) + PYTHON + end + # Prepend client if requested + if @args.raw['include_client_setup'] + client_setup + call_code + else + call_code + end end end \ No newline at end of file diff --git a/spec-insert/lib/renderers/templates/example_code.mustache b/spec-insert/lib/renderers/templates/example_code.mustache index f7eb4ae4fd4..19ac77add65 100644 --- a/spec-insert/lib/renderers/templates/example_code.mustache +++ b/spec-insert/lib/renderers/templates/example_code.mustache @@ -11,7 +11,6 @@ {% endcapture %} {% include code-block.html - rest=step1_rest - python=step1_python - javascript=step1_javascript +rest=step1_rest +python=step1_python %} \ No newline at end of file