diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 4e374f67386..820ea0bd97a 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -178,3 +178,12 @@ bundle exec rake generate_utilization_coverage The coverage report will be generated in the `spec-insert/utilization_coverage.md` by default. +## Spec insert generate dry-run report + +To generate a dry-run report of all APIs with all available spec insert components, run the following command: + +```shell +cd spec-insert +bundle exec rake generate_dry_run_report +``` +This will also generate a markdown (.md) file for each API with their rendered components in the `spec-insert/dry_run` folder. This allows you to preview the rendered components for all APIs without modifying the original documentation files. A report summarizing the errors found during the dry-run will be generated in the `spec-insert/dry_run_report.md` file. diff --git a/spec-insert/.gitignore b/spec-insert/.gitignore index b14907f21ea..a28b2120e4c 100644 --- a/spec-insert/.gitignore +++ b/spec-insert/.gitignore @@ -1,3 +1,5 @@ opensearch-openapi.yaml rspec_examples.txt -utilization_coverage.md \ No newline at end of file +utilization_coverage.md +dry_run_report.md +dry-run/ diff --git a/spec-insert/Rakefile b/spec-insert/Rakefile index c60443fecb3..6bcd414c6ea 100644 --- a/spec-insert/Rakefile +++ b/spec-insert/Rakefile @@ -8,8 +8,11 @@ require 'rake' require 'active_support/all' -require_relative 'lib/coverage/utilization_coverage' +require_relative 'lib/reports/utilization_coverage' +require_relative 'lib/reports/dry_run_report' require_relative 'lib/utils' +require_relative 'lib/renderers/spec_insert' +require_relative 'lib/insert_arguments' desc 'Generate utilization coverage of Spec-Insert components' task :generate_utilization_coverage do @@ -19,3 +22,23 @@ task :generate_utilization_coverage do File.write(file, coverage) puts "Utilization coverage written to #{file}" end + +desc 'Generate all Spec-Insert components for all APIs and summarize the results' +task :generate_dry_run_report do + Utils.load_spec + report = DryRunReport.new.render + file = File.join(__dir__, 'dry_run_report.md') + File.write(file, report) + puts "Dry run report written to #{file}" +end + +desc 'Generate a specific component into the console' +task :dry_run_generate, [:api, :component] do |_, args| + Utils.load_spec + render = SpecInsert.new(InsertArguments.new(args)).render + output = "./dry-run/_#{args[:api]}_#{args[:component]}.md" + File.write(output, render) + + puts render + puts "\n\nThe above render has been written to #{output}" +end diff --git a/spec-insert/lib/api/action.rb b/spec-insert/lib/api/action.rb index 99c3ad24b74..f0a8fdcf1df 100644 --- a/spec-insert/lib/api/action.rb +++ b/spec-insert/lib/api/action.rb @@ -60,24 +60,23 @@ def path_parameters .map { |params| Parameter.from_param_specs(params, @operations.size) } end - # @return [Api::Body, nil] Request body + # @return [Api::Body] Request body def request_body - @request_body ||= - begin - operation = @operations.find { |op| op.spec.requestBody.present? } - required = @operations.all? { |op| op.spec.requestBody.required } - operation.nil? ? nil : Body.new(operation.spec.requestBody.content, required:) - end + @request_body ||= begin + operation = @operations.find { |op| op.spec.requestBody.present? } + required = @operations.all? { |op| op.spec.requestBody&.required } + content = operation ? operation.spec.requestBody.content : nil + Body.new(content, required:) + end end # @return [Api::Body] Response body def response_body - @response_body ||= - begin - spec = @operations.first.spec - code = SUCCESS_CODES.find { |c| spec.responses[c].present? } - Body.new(@operations.first.spec.responses[code].content, required: nil) - end + @response_body ||= begin + spec = @operations.first.spec + code = SUCCESS_CODES.find { |c| spec.responses[c].present? } + Body.new(spec.responses[code].content, required: nil) + end end # @return [String] Full name of the action (i.e. namespace.action) diff --git a/spec-insert/lib/api/body.rb b/spec-insert/lib/api/body.rb index e20f588cc07..ec396ca6dd3 100644 --- a/spec-insert/lib/api/body.rb +++ b/spec-insert/lib/api/body.rb @@ -6,46 +6,27 @@ module Api # Request or response body class Body - # @return [Boolean] Whether the body is in NDJSON format - attr_reader :ndjson - + # @param [Boolean] empty whether a schema is defined + attr_reader :empty # @return [Boolean] attr_reader :required - # @return [Array] - attr_reader :params_group - - # @param [SpecHash] content + # @param [SpecHash, nil] content # @param [Boolean, nil] required def initialize(content, required:) @required = required - @ndjson = content['application/json'].nil? - spec = content['application/json'] || content['application/x-ndjson'] - @params_group = BodyParameterGroup.from_schema( - flatten_schema(spec.schema), - description: spec.description || spec.schema.description, - ancestors: [] - ) + content ||= {} + @spec = content['application/json'] || content['application/x-ndjson'] + @empty = @spec&.schema.nil? end - # @param [SpecHash] schema - # @return [SpecHash] a schema with allOf flattened - def flatten_schema(schema) - return schema if schema.type.present? && schema.type != 'object' - return schema if schema.properties.present? - return schema if schema.additionalProperties.present? - return schema.anyOf.map { |sch| flatten_schema(sch) } if schema.anyOf.present? - return schema.oneOf.map { |sch| flatten_schema(sch) } if schema.oneOf.present? - return schema if schema.allOf.blank? - - schema = schema.allOf.each_with_object({ properties: {}, required: [] }) do |sch, h| - sch = flatten_schema(sch) - h[:properties].merge!(sch.properties || {}) - h[:required] += sch.required || [] - h[:additionalProperties] ||= sch.additionalProperties - end - - SpecHash.new(schema, fully_parsed: true) + # @return [Api::BodyParameterGroup] + def params_group + @params_group ||= BodyParameterGroup.new( + schema: @spec.schema, + description: @spec.description || @spec.schema.description, + ancestors: [] + ) end end end diff --git a/spec-insert/lib/api/body_parameter.rb b/spec-insert/lib/api/body_parameter.rb index 1aaf3b221e8..cf2fdf6786f 100644 --- a/spec-insert/lib/api/body_parameter.rb +++ b/spec-insert/lib/api/body_parameter.rb @@ -6,50 +6,67 @@ module Api # Represents a group of parameters of an object within a request or response body class BodyParameterGroup - def self.from_schema(schema, description:, ancestors:) - is_array = schema.type == 'array' || schema.items.present? - parameters = BodyParameter.from_schema(is_array ? schema.items : schema) - new(parameters:, ancestors:, description:, is_array:) - end - - attr_reader :parameters, :ancestors, :description, :is_array + attr_reader :members, :ancestors, :description, :is_array, :is_nested, :schema - # @param [Array] parameters + # @param [SpecHash] schema schema of an object or an array of objects # @param [Array] ancestors # @param [String] description - # @param [Boolean] is_array - def initialize(parameters:, ancestors:, description:, is_array:) - @parameters = parameters + def initialize(schema:, ancestors:, description:) @ancestors = ancestors @description = description - @is_array = is_array - parameters.each { |param| param.group = self } + @is_array = schema.items.present? + @schema = @is_array ? schema.items : schema + @schema = flatten_schema(@schema) + @members = parse_members(@schema) + @is_nested = @members.any? { |param| param.is_a?(BodyParameterGroup) } + members.each { |param| param.group = self } unless @is_nested end # @return [Array] The child groups of the group - def descendants - @parameters.map(&:child_params_group).compact.flat_map do |group| - [group] + group.descendants + def descendants(seen_schemas = Set.new([@schema])) + child_groups = @is_nested ? @members : @members.map(&:child_params_group).compact + child_groups.reject { |g| seen_schemas.include?(g.schema) }.flat_map do |group| + seen_schemas.add(group.schema) + [group] + group.descendants(seen_schemas) end end - end - # TODO: Handle cyclic references - # Represents a body parameter of different levels of a request or response body - class BodyParameter < Parameter - # @param [SpecHash] schema The schema of an object - # @return [Array] The parameters of the object - def self.from_schema(schema) + # @param [SpecHash] schema + # @return [Array, Array b.name } return parameters unless schema.additionalProperties - additional_schema = schema.additionalProperties == true ? {} : schema.additionalProperties + additional_schema = schema.additionalProperties == true ? SpecHash.new({}) : schema.additionalProperties free_form_name = CONFIG.param_table.parameter_column.freeform_text - parameters + [BodyParameter.new(name: free_form_name, schema: SpecHash.new(additional_schema))] + parameters + [BodyParameter.new(name: free_form_name, schema: additional_schema)] + end + + # @param [SpecHash] schema + # @return [SpecHash] a schema with allOf flattened + def flatten_schema(schema) + return schema if schema.allOf.blank? + + schema = schema.allOf.each_with_object({ 'properties' => {}, 'required' => [] }) do |sch, h| + sch = flatten_schema(sch) + h['properties'].merge!(sch.properties || {}) + h['required'] += sch.required || [] + h['additionalProperties'] ||= sch.additionalProperties + end + + SpecHash.new(schema, fully_parsed: true) end + end + # TODO: Handle cyclic references + # Represents a body parameter of different levels of a request or response body + class BodyParameter < Parameter attr_accessor :group # @param [String] name @@ -67,12 +84,12 @@ def initialize(name:, schema:, required: false) @include_object = @doc_type.include?('Object') end - # @return [BodyParameterGroup, nil] The parameters of the object + # @return [BodyParameterGroup, nil] The parameters group of an object parameter def child_params_group return nil unless @include_object return @child_params_group if defined?(@child_params_group) - @child_params_group ||= BodyParameterGroup.from_schema( - @schema, + @child_params_group ||= BodyParameterGroup.new( + schema: @schema, ancestors: @group.ancestors + [@name], description: @description ) @@ -82,6 +99,7 @@ def child_params_group # TODO: Turn this into a configurable setting def parse_array(schema) + return 'Array' if schema.items == true || schema.items.nil? "Array of #{parse_doc_type(schema.items).pluralize}" end end diff --git a/spec-insert/lib/api/parameter.rb b/spec-insert/lib/api/parameter.rb index 77143c9f71a..7dda808c70b 100644 --- a/spec-insert/lib/api/parameter.rb +++ b/spec-insert/lib/api/parameter.rb @@ -82,6 +82,7 @@ def parse_doc_type(schema) return parse_array(schema) if type == 'array' || schema.items.present? return 'NULL' if type == 'null' return 'Object' if type == 'object' || type.nil? + return type.map { |t| parse_doc_type(SpecHash.new({ 'type' => t })) }.uniq.sort.join(' or ') if type.is_a?(Array) raise "Unhandled JSON Schema Type: #{type}" end diff --git a/spec-insert/lib/doc_processor.rb b/spec-insert/lib/doc_processor.rb index 5888d630d67..caae1ff5d0f 100644 --- a/spec-insert/lib/doc_processor.rb +++ b/spec-insert/lib/doc_processor.rb @@ -3,6 +3,7 @@ require 'pathname' require_relative 'renderers/spec_insert' require_relative 'spec_insert_error' +require_relative 'insert_arguments' # Processes a file, replacing spec-insert blocks with rendered content class DocProcessor @@ -51,7 +52,8 @@ def find_insertions(lines) validate_markers!(start_indices, end_indices) start_indices.zip(end_indices).map do |start, finish| - [start, finish, SpecInsert.new(lines[start..finish])] + args = InsertArguments.from_marker(lines[start..finish]) + [start, finish, SpecInsert.new(args)] end end diff --git a/spec-insert/lib/insert_arguments.rb b/spec-insert/lib/insert_arguments.rb index 9d10314fb77..aa05cc01df1 100644 --- a/spec-insert/lib/insert_arguments.rb +++ b/spec-insert/lib/insert_arguments.rb @@ -7,13 +7,20 @@ class InsertArguments attr_reader :raw + # @param [Hash] args raw arguments read from the doc insert marker + def initialize(args) + @raw = args.to_h.with_indifferent_access + end + # @param [Array] lines the lines between "" - def initialize(lines) + # @return [InsertArguments] + def self.from_marker(lines) end_index = lines.each_with_index.find { |line, _index| line.match?(/^\s*-->/) }&.last&.- 1 - @raw = lines[1..end_index].filter { |line| line.include?(':') }.to_h do |line| + args = lines[1..end_index].filter { |line| line.include?(':') }.to_h do |line| key, value = line.split(':') [key.strip, value.strip] end + new(args) end # @return [String] diff --git a/spec-insert/lib/renderers/body_parameters.rb b/spec-insert/lib/renderers/body_parameters.rb index abc7d16887a..fbb678a45c7 100644 --- a/spec-insert/lib/renderers/body_parameters.rb +++ b/spec-insert/lib/renderers/body_parameters.rb @@ -11,7 +11,7 @@ def initialize(action, args, is_request:) super(action, args) @is_request = is_request @body = is_request ? @action.request_body : @action.response_body - @params_group = @body.params_group + @empty = @body.empty end def header @@ -21,33 +21,40 @@ def header def description name = "The #{@is_request ? 'request' : 'response'} body" required = @body.required ? ' is __required__. It' : ' is optional. It' if @is_request - schema_desc = if @params_group.is_array + schema_desc = if @body.params_group.is_array "#{name}#{required} is an __array of JSON objects__ (NDJSON). Each object has the following fields." else "#{name}#{required} is a JSON object with the following fields." end - [@params_group.description, schema_desc].compact.reject(&:empty?).join("\n\n") + [@body.params_group.description, schema_desc].compact.reject(&:empty?).join("\n\n") end def required @body.required end - def table - ParameterTableRenderer.new(@params_group.parameters, @args, is_body: true).render + def root_tables + render_tables(@body.params_group) end def descendants - @params_group.descendants.sort_by(&:ancestors).map do |group| + @body.params_group.descendants.map do |group| { block_name: "#{@args.api}::#{@is_request ? 'request' : 'response'}_body", summary: "#{header}: #{group.ancestors.join(' > ')}", description: descendant_desc(group), - table: ParameterTableRenderer.new(group.parameters, @args, is_body: true).render } + descendant_tables: render_tables(group) } end end private + # @param [Api::BodyParameterGroup] group + # @return [Array] + def render_tables(group) + return group.members.flat_map { |g| render_tables(g) } if group.is_nested + [ParameterTableRenderer.new(group.members, @args, is_body: true).render] + end + # @param [Api::BodyParameterGroup] group def descendant_desc(group) schema_desc = diff --git a/spec-insert/lib/renderers/components/base_mustache_renderer.rb b/spec-insert/lib/renderers/components/base_mustache_renderer.rb index 920ba704fb3..6129bfec146 100644 --- a/spec-insert/lib/renderers/components/base_mustache_renderer.rb +++ b/spec-insert/lib/renderers/components/base_mustache_renderer.rb @@ -19,6 +19,10 @@ def initialize(action, args) @args = args end + def render + @empty ? nil : super + end + def omit_header @args.omit_header end diff --git a/spec-insert/lib/renderers/spec_insert.rb b/spec-insert/lib/renderers/spec_insert.rb index 77f13dd049c..87289a9baf8 100644 --- a/spec-insert/lib/renderers/spec_insert.rb +++ b/spec-insert/lib/renderers/spec_insert.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require_relative 'components/base_mustache_renderer' -require_relative '../insert_arguments' require_relative '../api/action' require_relative '../spec_insert_error' require_relative 'endpoints' @@ -13,9 +12,8 @@ class SpecInsert < BaseMustacheRenderer self.template_file = "#{__dir__}/templates/spec_insert.mustache" - # @param [Array] lines the lines between "" - def initialize(lines) - args = InsertArguments.new(lines) + # @param [InsertArguments] + def initialize(args) action = Api::Action.by_full_name[args.api] super(action, args) raise SpecInsertError, '`api` argument not specified.' unless @args.api @@ -26,6 +24,9 @@ def arguments @args.raw.map { |key, value| { key:, value: } } end + def api; @args.api end + def component; @args.component end + def content raise SpecInsertError, '`component` argument not specified.' unless @args.component case @args.component.to_sym diff --git a/spec-insert/lib/renderers/templates/body_parameters.mustache b/spec-insert/lib/renderers/templates/body_parameters.mustache index ae6a6ddc867..c8831edfb41 100644 --- a/spec-insert/lib/renderers/templates/body_parameters.mustache +++ b/spec-insert/lib/renderers/templates/body_parameters.mustache @@ -3,7 +3,7 @@ {{{description}}} {{/omit_header}} -{{{table}}}{{#descendants}} +{{#root_tables}}{{{.}}}{{/root_tables}}{{#descendants}}
{{{summary}}} @@ -11,5 +11,5 @@ {: .text-delta} {{{description}}} -{{{table}}} +{{#descendant_tables}}{{{.}}}{{/descendant_tables}}
{{/descendants}} \ No newline at end of file diff --git a/spec-insert/lib/renderers/templates/spec_insert.mustache b/spec-insert/lib/renderers/templates/spec_insert.mustache index 63b6323d48f..f0ad64d0719 100644 --- a/spec-insert/lib/renderers/templates/spec_insert.mustache +++ b/spec-insert/lib/renderers/templates/spec_insert.mustache @@ -3,5 +3,10 @@ {{{key}}}: {{{value}}} {{/arguments}} --> -{{{content}}} +{{#content}} +{{{.}}} +{{/content}} +{{^content}} + +{{/content}} diff --git a/spec-insert/lib/reports/dry_run.rb b/spec-insert/lib/reports/dry_run.rb new file mode 100644 index 00000000000..06e51c63d86 --- /dev/null +++ b/spec-insert/lib/reports/dry_run.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'mustache' +require_relative '../utils' +require_relative '../renderers/spec_insert' +require_relative '../insert_arguments' + +# Generate a dry run for a specific API action +class DryRun < Mustache + self.template_path = "#{__dir__}/templates" + self.template_file = "#{__dir__}/templates/dry_run.mustache" + + # @param [Api::Action] action + # @param [Hash{String => String[]] errors_report + def initialize(action:, errors_report:) + super + @action = action + @errors_report = errors_report + puts "Generating dry run for #{action.full_name}" + end + + def api_name + @action.full_name + end + + def components + ::Utils::COMPONENTS.map do |id, name| + args = InsertArguments.new(api: @action.full_name, component: id) + { component: SpecInsert.new(args).render, error: false } + rescue StandardError, SystemStackError => e + @errors_report[id] << { api: @action.full_name, message: e.message } + { message: e.message, component: name, error: true } + end + end +end diff --git a/spec-insert/lib/reports/dry_run_report.rb b/spec-insert/lib/reports/dry_run_report.rb new file mode 100644 index 00000000000..ce165a04361 --- /dev/null +++ b/spec-insert/lib/reports/dry_run_report.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative '../utils' +require_relative '../api/action' +require_relative 'dry_run' + +# Generate a dry run report for all API actions +class DryRunReport < Mustache + self.template_path = "#{__dir__}/templates" + self.template_file = "#{__dir__}/templates/dry_run_report.mustache" + + OUTPUT_DIR = File.join(::Utils::SPEC_INSERT_DIR, 'dry-run') + + def initialize + super + @errors_report = Hash.new { |hash, key| hash[key] = [] } + generate_dry_runs + end + + def any_errors + @errors_report.keys.any? + end + + def errors + ::Utils::COMPONENTS.map do |comp_id, comp_name| + { component_name: comp_name, + component_id: comp_id, + apis: @errors_report[comp_id], + error_count: @errors_report[comp_id].count } + end + end + + private + + def generate_dry_runs + FileUtils.rm_rf(OUTPUT_DIR) + FileUtils.mkdir_p(OUTPUT_DIR) + Api::Action.all.each do |action| + dry_run = DryRun.new(action:, errors_report: @errors_report) + file = File.join(OUTPUT_DIR, "#{action.full_name}.md") + File.write(file, dry_run.render) + end + end +end diff --git a/spec-insert/lib/reports/templates/dry_run.mustache b/spec-insert/lib/reports/templates/dry_run.mustache new file mode 100644 index 00000000000..523e6ba8c24 --- /dev/null +++ b/spec-insert/lib/reports/templates/dry_run.mustache @@ -0,0 +1,20 @@ +# Rendered Components of API `{{{api_name}}}` + +{{#components}} +{{#error}} + +--- + +### Component `{{{component}}}` failed to render due to the following error: + +``` +{{{message}}} +``` + +--- + +{{/error}} +{{^error}} +{{{component}}} +{{/error}} +{{/components}} diff --git a/spec-insert/lib/reports/templates/dry_run_report.mustache b/spec-insert/lib/reports/templates/dry_run_report.mustache new file mode 100644 index 00000000000..dfa7d32f60b --- /dev/null +++ b/spec-insert/lib/reports/templates/dry_run_report.mustache @@ -0,0 +1,24 @@ +# Dry Run Report for Spec Insert + +A file containing all rendered components for API has been created in the [dry run directory](./dry-run). +{{#any_errors}} +Below are the components that have errors in the dry run and the commands that will reproduce them. + +{{#errors}} + +## {{{component_name}}}: {{error_count}} errors +{{#apis}} +### {{{api}}} + +
{{{message}}}
+ +```shell + bundle exec rake dry_run_generate[{{{api}}},{{{component_id}}}] +``` +{{/apis}} +{{/errors}} +{{/any_errors}} + +{{^any_errors}} +No errors occurred during the dry run :D +{{/any_errors}} diff --git a/spec-insert/lib/coverage/templates/utilization_coverage.mustache b/spec-insert/lib/reports/templates/utilization_coverage.mustache similarity index 100% rename from spec-insert/lib/coverage/templates/utilization_coverage.mustache rename to spec-insert/lib/reports/templates/utilization_coverage.mustache diff --git a/spec-insert/lib/coverage/utilization_coverage.rb b/spec-insert/lib/reports/utilization_coverage.rb similarity index 62% rename from spec-insert/lib/coverage/utilization_coverage.rb rename to spec-insert/lib/reports/utilization_coverage.rb index 88268e31a3e..a6ee41014a5 100644 --- a/spec-insert/lib/coverage/utilization_coverage.rb +++ b/spec-insert/lib/reports/utilization_coverage.rb @@ -12,7 +12,7 @@ class UtilizationCoverage < Mustache def components total = Api::Action.all.count ::Utils::COMPONENTS.map do |id, component| - utilization = ::Utils.utilized_components.values.flatten.count { |comp| comp == id } + utilization = utilized_components.values.flatten.count { |comp| comp == id } percent = (utilization.to_f / total * 100).round(2) { component:, utilization:, total:, percent:, namespaces: namespace_utilization(id) } end @@ -25,7 +25,7 @@ def namespace_utilization(component) namespace = '[root]' unless namespace.present? actions = actions.map do |action| { name: action.full_name, - utilized: ::Utils.utilized_components[action.full_name]&.include?(component) } + utilized: utilized_components[action.full_name]&.include?(component) } end.sort_by { |action| action[:name] } total = actions.count utilization = actions.count { |action| action[:utilized] } @@ -33,4 +33,14 @@ def namespace_utilization(component) { namespace:, utilization:, total:, percent:, actions: } end end + + # @return [Hash] where each is an API/action name and each value is an array of generated component for that API + def utilized_components + @utilized_components ||= begin + logger = Logger.new(IO::NULL) + spec_inserts = ::Utils.target_files.flat_map { |file| DocProcessor.new(file, logger:).spec_inserts } + Set.new(spec_inserts.map { |insert| [insert.args.api, insert.args.component] }) + .to_a.group_by(&:first).transform_values { |values| values.map(&:last) } + end + end end diff --git a/spec-insert/lib/utils.rb b/spec-insert/lib/utils.rb index 3131eb90fe8..22bd273db2b 100644 --- a/spec-insert/lib/utils.rb +++ b/spec-insert/lib/utils.rb @@ -2,12 +2,12 @@ require 'yaml' require_relative 'spec_hash' -require_relative 'doc_processor' # Utility methods for the Spec-Insert module Utils REPO_ROOT = File.expand_path('../..', __dir__) - SPEC_FILE = File.join(REPO_ROOT, 'spec-insert/opensearch-openapi.yaml') + SPEC_INSERT_DIR = File.join(REPO_ROOT, 'spec-insert') + SPEC_FILE = File.join(SPEC_INSERT_DIR, 'opensearch-openapi.yaml') COMPONENTS = { 'endpoints' => 'Endpoints', 'query_parameters' => 'Query Parameters', @@ -43,21 +43,16 @@ def self.download_spec(forced: false, logger: nil) "-o #{SPEC_FILE}" end - # @return [Hash] where each is an API/action name and each value is an array of generated component for that API - def self.utilized_components - @utilized_components ||= begin - logger = Logger.new(IO::NULL) - spec_inserts = target_files.flat_map { |file| DocProcessor.new(file, logger:).spec_inserts } - Set.new(spec_inserts.map { |insert| [insert.args.api, insert.args.component] }) - .to_a.group_by(&:first).transform_values { |values| values.map(&:last) } - end - end - - # @param [String] value - # @return [Boolean] - def self.parse_boolean(value) - return true if value == true || value =~ /^(true|t|yes|y|1)$/i - return false if value == false || value.nil? || value =~ /^(false|f|no|n|0)$/i - raise ArgumentError, "invalid value for Boolean: #{value}" + # @param [String] api + # @param [String] component + # @return [Array] lines representing dummy marker used as input for SpecInsert + def self.dummy_marker(api, component) + [ + '', + '' + ] end end