require "json" require "yaml" require "cgi" require "rspec_api_documentation/writers/json_writer" module RspecApiDocumentation module Writers class OpenApiWriter < Writer attr_writer :types, :swagger def write File.open(configuration.docs_dir.join("open_api.json"), "w") do |f| f.write JSON.pretty_generate(get_hash) end File.open(configuration.docs_dir.join("open_api.yaml"), "w") do |f| f.write get_hash.to_yaml end end def get_hash index.examples.each do |rspec_example| api = JSONExample.new(rspec_example, configuration).as_json.deep_stringify_keys description = api["description"] route = api["route"].gsub(%r{\:version\/}, "") route = route.gsub(%r{\:([^\/]+)}, '{\1}') params = route.scan(%r{\{([^\/]+)\}}).map { |param| { "name" => param[0], "in" => "path", "required" => true, "schema" => { "type" => "string" } } } swagger["paths"][route] = swagger["paths"][route] || {} responses = {} req = api["requests"][0] method = req["request_method"].downcase result = JSON.parse(req["response_body"]) if req["response_body"] properties = {} result&.each { |k, v| properties[k] = get_properties(v) } responses[req["response_status"]] = { "description" => req["response_status_text"], "headers" => req["response_headers"].map { |name, _v| [name, { "schema" => { "type" => "string" } }] }.to_h, "content" => { "application/json" => { "schema" => { "type" => "object", "properties" => properties } } } } api["parameters"]&.each do |param| params.push("name" => param["name"], "in" => "query", "schema" => { "type" => "string" }, "description" => param["description"], "required" => param["required"] || false) end req["request_query_parameters"]&.each do |name, value| params.push("name" => name, "in" => "query", "schema" => { "type" => "string" }, "example" => value) end if req["request_content_type"] == "application/x-www-form-urlencoded" && api["requests"][0]["request_body"] req["request_body"].scan(/([^\&\=]*)=([^\&]*)/).map do |body| params.push("name" => body[0], "in" => "query", "schema" => { "type" => "string" }, "example" => CGI.unescape(body[1])) end end swagger["paths"][route][method] = swagger["paths"][route][method] || {} swagger["paths"][route][method]["parameters"] = swagger["paths"][route][method]["parameters"] || [] req["request_headers"]&.each do |name, value| if name == "Authorization" swagger["paths"][route][method]["security"] = if /Bearer (.*)/.match?(value) unless responses[401] && responses["401"] responses[401] = { "description" => "Unauthorized Access", "content" => { "application/json" => { "schema" => { "$ref" => "#/components/schemas/unauthorized" } } } } end [{ "bearerAuth" => [] }] else [{ "basicAuth" => [] }] end else params.unshift("name" => name, "in" => "header", "schema" => { "type" => "string" }, "example" => value, "required" => true) end end params.each do |param| has = false swagger["paths"][route][method]["parameters"].each_with_index do |p, i| if p["name"] == param["name"] swagger["paths"][route][method]["parameters"][i]["example"] = swagger["paths"][route][method]["parameters"][i]["example"] || param["example"] has = true end end swagger["paths"][route][method]["parameters"].push(param) unless has end desc = swagger["paths"][route][method]["description"] || "" swagger["paths"][route][method]["description"] = desc + "- #{description}" if params.empty? swagger["paths"][route][method]["description"] += "\n" else swagger["paths"][route][method]["description"] += ", Needed Parameters:\n - #{params.map{ |p| p['name'] }.join("\n - ")} \n" end swagger["paths"][route][method]["responses"] = responses end swagger["components"]["schemas"] = types swagger end def get_properties(v) case v.class.name when "Hash" props = v.map { |key, value| [key, get_properties(value)] }.to_h x = { "type" => "object", "properties" => props } if v["type"] type = v["type"].tr("\/", "_") types[type] = x unless types.key?(type) if types.key?(type) types[type]["properties"] = hash_deep_assign(types[type]["properties"], x["properties"]) else types[type] = x end return { "$ref" => "#/components/schemas/#{type}" } end x when "Array" { "type" => "array", "items" => get_properties(v[0]) } when "TrueClass", "FalseClass" { "type" => "boolean", "example" => v } when "NilClass" { "type" => "integer", "nullable" => true } when "Integer", "Float" { "type" => "number", "example" => v } else { "type" => v.class.name.downcase, "example" => v } end end def hash_deep_assign(target, other) return target unless target && other other.each do |key, value| target[key] = if target.key?(key) && other[key].is_a?(Hash) hash_deep_assign(target[key], other[key]) elsif target.key?(key) && target[key].is_a?(Array) && other[key].is_a?(Array) target[key].push(*other[key]).uniq elsif key == "$ref" if other["$ref"] && target["$ref"] && target["$ref"] != other["$ref"] target = { "oneOf" => [ { "$ref" => target["$ref"] }, { "$ref" => other["$ref"] } ] } elsif target.key?("oneOf") target["oneOf"].push("$ref" => other["$ref"]).uniq else target["$ref"] || other["$ref"] end elsif target.key?(key) target[key] || other[key] else value end end target = { "oneOf" => target["oneOf"] } if target.key?("oneOf") target = { "$ref" => target["$ref"] } if target.key?("$ref") target = get_properties(target["example"]) if target["example"] && target.key?("nullable") target end def types @types ||= { "unauthorized" => { "properties" => { "code" => { "type" => "string", "example" => "invalid_client_credentials" }, "message" => { "type" => "string", "example" => "Not found or invalid client credentials" } } } } end def swagger @swagger ||= { "openapi" => "3.0.0", "info" => info, "servers" => servers, "paths" => {}, "components" => { "securitySchemes" => { "bearerAuth" => { "type" => "http", "scheme" => "bearer" }, "basicAuth" => { "type" => "http", "scheme" => "basic" } }, "schemas" => {} } } end def info configs["info"] end def servers configs["servers"] end def configs (defined_configs || default_configs).deep_stringify_keys end def defined_configs configuration.open_api end def default_configs { info: { version: "1.0.0", title: "Open API", description: "Open API", contact: { name: "OpenAPI" } }, servers: [ { url: "http://localhost:{port}", description: "Development server", variables: { port: { default: "3000" } } } ] } end end end end