From b095c2c35e3fcea1c747ede2c916925d9761339a Mon Sep 17 00:00:00 2001 From: sourav chakraborty Date: Fri, 25 Aug 2023 16:58:03 +0530 Subject: [PATCH] Add start and end line columns including yml support --- openapi/table_openapi_component_header.go | 29 ++++- openapi/table_openapi_component_parameter.go | 29 ++++- .../table_openapi_component_request_body.go | 37 ++++-- openapi/table_openapi_component_response.go | 25 +++- openapi/table_openapi_component_schema.go | 29 ++++- ...table_openapi_component_security_scheme.go | 29 ++++- openapi/table_openapi_info.go | 11 +- openapi/table_openapi_path.go | 10 +- openapi/table_openapi_path_request_body.go | 10 +- openapi/table_openapi_path_response.go | 10 +- openapi/table_openapi_server.go | 11 +- openapi/utils.go | 121 ++++++++++++++++-- 12 files changed, 304 insertions(+), 47 deletions(-) diff --git a/openapi/table_openapi_component_header.go b/openapi/table_openapi_component_header.go index beae9f0..b82f40c 100644 --- a/openapi/table_openapi_component_header.go +++ b/openapi/table_openapi_component_header.go @@ -2,6 +2,8 @@ package openapi import ( "context" + "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -21,7 +23,7 @@ func tableOpenAPIComponentHeader(ctx context.Context) *plugin.Table { Hydrate: listOpenAPIComponentHeaders, KeyColumns: plugin.OptionalColumns([]string{"path"}), }, - Columns: []*plugin.Column{ + Columns: openAPICommonColumns([]*plugin.Column{ {Name: "key", Description: "The key used to refer or search the header.", Type: proto.ColumnType_STRING}, {Name: "name", Description: "The name of the header.", Type: proto.ColumnType_STRING}, {Name: "location", Description: "The location of the header. Possible values are query, header, path or cookie.", Type: proto.ColumnType_STRING, Transform: transform.FromField("In").NullIfZero()}, @@ -35,13 +37,15 @@ func tableOpenAPIComponentHeader(ctx context.Context) *plugin.Table { {Name: "schema", Description: "The schema of the header.", Type: proto.ColumnType_JSON, Transform: transform.FromField("Schema.Value")}, {Name: "schema_ref", Description: "The schema reference of the header.", Type: proto.ColumnType_STRING, Transform: transform.FromField("Schema.Ref").Transform(transform.NullIfZeroValue)}, {Name: "path", Description: "Path to the file.", Type: proto.ColumnType_STRING}, - }, + }), } } type openAPIComponentHeader struct { - Path string - Key string + Path string + Key string + StartLine int + EndLine int openapi3.Header } @@ -52,6 +56,12 @@ func listOpenAPIComponentHeaders(ctx context.Context, d *plugin.QueryData, h *pl // available by the optional key column path := h.Item.(filePath).Path + file, err := os.Open(path) + if err != nil { + plugin.Logger(ctx).Error("openapi_component_header.listOpenAPIComponentHeaders", "file_open_error", err) + return nil, err + } + // Get the parsed contents doc, err := getDoc(ctx, d, path) if err != nil { @@ -66,7 +76,16 @@ func listOpenAPIComponentHeaders(ctx context.Context, d *plugin.QueryData, h *pl // For each header, scan its arguments for k, v := range doc.Components.Headers { - d.StreamListItem(ctx, openAPIComponentHeader{path, k, *v.Value}) + + // fetch start and end line for each header + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "components", k) + } else { + startLine, endLine = findBlockLinesFromYML(file, "components", k) + } + + d.StreamListItem(ctx, openAPIComponentHeader{path, k, startLine, endLine, *v.Value}) // Context may get cancelled due to manual cancellation or if the limit has been reached if d.RowsRemaining(ctx) == 0 { diff --git a/openapi/table_openapi_component_parameter.go b/openapi/table_openapi_component_parameter.go index 7753fe4..9b7762e 100644 --- a/openapi/table_openapi_component_parameter.go +++ b/openapi/table_openapi_component_parameter.go @@ -2,6 +2,8 @@ package openapi import ( "context" + "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -21,7 +23,7 @@ func tableOpenAPIComponentParameter(ctx context.Context) *plugin.Table { Hydrate: listOpenAPIComponentParameters, KeyColumns: plugin.OptionalColumns([]string{"path"}), }, - Columns: []*plugin.Column{ + Columns: openAPICommonColumns([]*plugin.Column{ {Name: "key", Description: "The key used to refer or search the parameter.", Type: proto.ColumnType_STRING}, {Name: "name", Description: "The name of the parameter.", Type: proto.ColumnType_STRING}, {Name: "location", Description: "The location of the parameter. Possible values are query, header, path or cookie.", Type: proto.ColumnType_STRING, Transform: transform.FromField("In")}, @@ -35,13 +37,15 @@ func tableOpenAPIComponentParameter(ctx context.Context) *plugin.Table { {Name: "schema", Description: "The schema of the parameter.", Type: proto.ColumnType_JSON, Transform: transform.FromField("Schema.Value")}, {Name: "schema_ref", Description: "The schema reference of the parameter.", Type: proto.ColumnType_STRING, Transform: transform.FromField("Schema.Ref").Transform(transform.NullIfZeroValue)}, {Name: "path", Description: "Path to the file.", Type: proto.ColumnType_STRING}, - }, + }), } } type openAPIComponentParameter struct { - Path string - Key string + Path string + Key string + StartLine int + EndLine int openapi3.Parameter } @@ -52,6 +56,12 @@ func listOpenAPIComponentParameters(ctx context.Context, d *plugin.QueryData, h // available by the optional key column path := h.Item.(filePath).Path + file, err := os.Open(path) + if err != nil { + plugin.Logger(ctx).Error("openapi_component_parameter.listOpenAPIComponentParameters", "file_open_error", err) + return nil, err + } + // Get the parsed contents doc, err := getDoc(ctx, d, path) if err != nil { @@ -66,7 +76,16 @@ func listOpenAPIComponentParameters(ctx context.Context, d *plugin.QueryData, h // For each parameter, scan its arguments for k, v := range doc.Components.Parameters { - d.StreamListItem(ctx, openAPIComponentParameter{path, k, *v.Value}) + + // fetch start and end line for each parameter + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "components", k) + } else { + startLine, endLine = findBlockLinesFromYML(file, "components", k) + } + + d.StreamListItem(ctx, openAPIComponentParameter{path, k, startLine, endLine, *v.Value}) // Context may get cancelled due to manual cancellation or if the limit has been reached if d.RowsRemaining(ctx) == 0 { diff --git a/openapi/table_openapi_component_request_body.go b/openapi/table_openapi_component_request_body.go index b6209e0..cf05028 100644 --- a/openapi/table_openapi_component_request_body.go +++ b/openapi/table_openapi_component_request_body.go @@ -2,6 +2,8 @@ package openapi import ( "context" + "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -20,21 +22,23 @@ func tableOpenAPIComponentRequestBody(ctx context.Context) *plugin.Table { Hydrate: listOpenAPIComponentRequestBodies, KeyColumns: plugin.OptionalColumns([]string{"path"}), }, - Columns: []*plugin.Column{ + Columns: openAPICommonColumns([]*plugin.Column{ {Name: "key", Description: "The key used to refer or search the request body.", Type: proto.ColumnType_STRING}, {Name: "description", Description: "A brief description of the request body.", Type: proto.ColumnType_STRING}, {Name: "required", Description: "True, if the request body is required.", Type: proto.ColumnType_BOOL}, {Name: "content", Description: "The content of the request body.", Type: proto.ColumnType_JSON}, {Name: "path", Description: "Path to the file.", Type: proto.ColumnType_STRING}, - }, + }), } } type openAPIComponentRequestBody struct { - Path string - Key string - Content []map[string]interface{} - Raw openapi3.RequestBody + Path string + Key string + StartLine int + EndLine int + Content []map[string]interface{} + Raw openapi3.RequestBody } //// LIST FUNCTION @@ -44,6 +48,12 @@ func listOpenAPIComponentRequestBodies(ctx context.Context, d *plugin.QueryData, // available by the optional key column path := h.Item.(filePath).Path + file, err := os.Open(path) + if err != nil { + plugin.Logger(ctx).Error("openapi_component_request_body.listOpenAPIComponentRequestBodies", "file_open_error", err) + return nil, err + } + doc, err := getDoc(ctx, d, path) if err != nil { plugin.Logger(ctx).Error("openapi_component_request_body.listOpenAPIComponentRequestBodies", "parse_error", err) @@ -57,9 +67,20 @@ func listOpenAPIComponentRequestBodies(ctx context.Context, d *plugin.QueryData, // For each request body, scan its arguments for k, v := range doc.Components.RequestBodies { + + // fetch start and end line for each requestBody + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "components", k) + } else { + startLine, endLine = findBlockLinesFromYML(file, "components", k) + } + requestBodyObject := openAPIComponentRequestBody{ - Path: path, - Key: k, + Path: path, + Key: k, + StartLine: startLine, + EndLine: endLine, } for header, content := range v.Value.Content { diff --git a/openapi/table_openapi_component_response.go b/openapi/table_openapi_component_response.go index e20699a..12e1498 100644 --- a/openapi/table_openapi_component_response.go +++ b/openapi/table_openapi_component_response.go @@ -2,6 +2,8 @@ package openapi import ( "context" + "os" + "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" @@ -20,14 +22,14 @@ func tableOpenAPIComponentResponse(ctx context.Context) *plugin.Table { Hydrate: listOpenAPIComponentResponses, KeyColumns: plugin.OptionalColumns([]string{"path"}), }, - Columns: []*plugin.Column{ + Columns: openAPICommonColumns([]*plugin.Column{ {Name: "key", Description: "The key of the response object definition.", Type: proto.ColumnType_STRING}, {Name: "description", Description: "A description of the response.", Type: proto.ColumnType_STRING}, {Name: "content", Description: "A map containing descriptions of potential response payloads.", Type: proto.ColumnType_JSON}, {Name: "headers", Description: "Maps a header name to its definition.", Type: proto.ColumnType_JSON, Transform: transform.FromField("Raw.Headers")}, {Name: "links", Description: "A map of operations links that can be followed from the response.", Type: proto.ColumnType_JSON, Transform: transform.FromField("Raw.Links")}, {Name: "path", Description: "Path to the file.", Type: proto.ColumnType_STRING}, - }, + }), } } @@ -36,6 +38,8 @@ type openAPIComponentResponse struct { Content []map[string]interface{} Key string Description string + StartLine int + EndLine int Raw openapi3.Response } @@ -46,6 +50,12 @@ func listOpenAPIComponentResponses(ctx context.Context, d *plugin.QueryData, h * // available by the optional key column path := h.Item.(filePath).Path + file, err := os.Open(path) + if err != nil { + plugin.Logger(ctx).Error("openapi_component_response.listOpenAPIComponentResponses", "file_open_error", err) + return nil, err + } + // Get the parsed contents doc, err := getDoc(ctx, d, path) if err != nil { @@ -60,10 +70,21 @@ func listOpenAPIComponentResponses(ctx context.Context, d *plugin.QueryData, h * // For each response, scan its arguments for k, v := range doc.Components.Responses { + + // fetch start and end line for each response + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "components", k) + } else { + startLine, endLine = findBlockLinesFromYML(file, "components", k) + } + responseObject := openAPIComponentResponse{ Path: path, Key: k, Description: *v.Value.Description, + StartLine: startLine, + EndLine: endLine, } for header, content := range v.Value.Content { diff --git a/openapi/table_openapi_component_schema.go b/openapi/table_openapi_component_schema.go index 214e209..20bc04c 100644 --- a/openapi/table_openapi_component_schema.go +++ b/openapi/table_openapi_component_schema.go @@ -2,6 +2,8 @@ package openapi import ( "context" + "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -21,7 +23,7 @@ func tableOpenAPIComponentSchema(ctx context.Context) *plugin.Table { Hydrate: listOpenAPIComponentSchemas, KeyColumns: plugin.OptionalColumns([]string{"path"}), }, - Columns: []*plugin.Column{ + Columns: openAPICommonColumns([]*plugin.Column{ {Name: "name", Description: "The name of the property.", Type: proto.ColumnType_STRING}, {Name: "type", Description: "The type of the schema.", Type: proto.ColumnType_STRING}, {Name: "format", Description: "The format of a specific schema type.", Type: proto.ColumnType_STRING}, @@ -57,13 +59,15 @@ func tableOpenAPIComponentSchema(ctx context.Context) *plugin.Table { {Name: "required", Description: "If true, the property must be defined.", Type: proto.ColumnType_JSON}, {Name: "properties", Description: "Describes the schema properties.", Type: proto.ColumnType_JSON}, {Name: "path", Description: "Path to the file.", Type: proto.ColumnType_STRING}, - }, + }), } } type openAPIComponentSchema struct { - Path string - Name string + Path string + Name string + StartLine int + EndLine int openapi3.Schema Properties map[string]interface{} } @@ -75,6 +79,12 @@ func listOpenAPIComponentSchemas(ctx context.Context, d *plugin.QueryData, h *pl // available by the optional key column path := h.Item.(filePath).Path + file, err := os.Open(path) + if err != nil { + plugin.Logger(ctx).Error("openapi_component_schema.listOpenAPIComponentSchemas", "file_open_error", err) + return nil, err + } + // Get the parsed contents doc, err := getDoc(ctx, d, path) if err != nil { @@ -89,11 +99,20 @@ func listOpenAPIComponentSchemas(ctx context.Context, d *plugin.QueryData, h *pl // For each schema, scan its arguments for k, v := range doc.Components.Schemas { + + // fetch start and end line for each schemas + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "components", k) + } else { + startLine, endLine = findBlockLinesFromYML(file, "components", k) + } + properties := map[string]interface{}{} for i, j := range v.Value.Properties { properties[i] = j.Value } - d.StreamListItem(ctx, openAPIComponentSchema{path, k, *v.Value, properties}) + d.StreamListItem(ctx, openAPIComponentSchema{path, k, startLine, endLine, *v.Value, properties}) // Context may get cancelled due to manual cancellation or if the limit has been reached if d.RowsRemaining(ctx) == 0 { diff --git a/openapi/table_openapi_component_security_scheme.go b/openapi/table_openapi_component_security_scheme.go index 61b70e0..4bef6c9 100644 --- a/openapi/table_openapi_component_security_scheme.go +++ b/openapi/table_openapi_component_security_scheme.go @@ -2,6 +2,8 @@ package openapi import ( "context" + "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -21,7 +23,7 @@ func tableOpenAPIComponentSecurityScheme(ctx context.Context) *plugin.Table { Hydrate: listOpenAPIComponentSecuritySchemes, KeyColumns: plugin.OptionalColumns([]string{"path"}), }, - Columns: []*plugin.Column{ + Columns: openAPICommonColumns([]*plugin.Column{ {Name: "key", Description: "The key used to refer or search the security scheme.", Type: proto.ColumnType_STRING}, {Name: "name", Description: "The name of the header, query or cookie parameter to be used.", Type: proto.ColumnType_STRING}, {Name: "type", Description: "The type of the security scheme. Valid values are apiKey, http, mutualTLS, oauth2, openIdConnect.", Type: proto.ColumnType_STRING}, @@ -32,13 +34,15 @@ func tableOpenAPIComponentSecurityScheme(ctx context.Context) *plugin.Table { {Name: "open_id_connect_url", Description: "OpenId Connect URL to discover OAuth2 configuration values.", Type: proto.ColumnType_STRING}, {Name: "flows", Description: "An object containing configuration information for the flow types supported.", Type: proto.ColumnType_JSON}, {Name: "path", Description: "Path to the file.", Type: proto.ColumnType_STRING}, - }, + }), } } type openAPIComponentSecurityScheme struct { - Path string - Key string + Path string + Key string + StartLine int + EndLine int openapi3.SecurityScheme } @@ -49,6 +53,12 @@ func listOpenAPIComponentSecuritySchemes(ctx context.Context, d *plugin.QueryDat // available by the optional key column path := h.Item.(filePath).Path + file, err := os.Open(path) + if err != nil { + plugin.Logger(ctx).Error("openapi_component_security_scheme.listOpenAPIComponentSecuritySchemes", "file_open_error", err) + return nil, err + } + // Get the parsed contents doc, err := getDoc(ctx, d, path) if err != nil { @@ -63,7 +73,16 @@ func listOpenAPIComponentSecuritySchemes(ctx context.Context, d *plugin.QueryDat // For each security scheme, scan its arguments for k, v := range doc.Components.SecuritySchemes { - d.StreamListItem(ctx, openAPIComponentSecurityScheme{path, k, *v.Value}) + + // fetch start and end line for each securitySchemes + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "components", k) + } else { + startLine, endLine = findBlockLinesFromYML(file, "components", k) + } + + d.StreamListItem(ctx, openAPIComponentSecurityScheme{path, k, startLine, endLine, *v.Value}) // Context may get cancelled due to manual cancellation or if the limit has been reached if d.RowsRemaining(ctx) == 0 { diff --git a/openapi/table_openapi_info.go b/openapi/table_openapi_info.go index 113f5d7..c956deb 100644 --- a/openapi/table_openapi_info.go +++ b/openapi/table_openapi_info.go @@ -3,6 +3,7 @@ package openapi import ( "context" "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -49,13 +50,19 @@ func listOpenAPIInfo(ctx context.Context, d *plugin.QueryData, h *plugin.Hydrate // available by the optional key column path := h.Item.(filePath).Path - // get the start and end lines of the info block file, err := os.Open(path) if err != nil { plugin.Logger(ctx).Error("openapi_info.listOpenAPIInfo", "file_open_error", err) return nil, err } - startLine, endLine := findBlockLines(file, "info", "") + + // fetch start and end line for info + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "info", "") + } else { + startLine, endLine = findBlockLinesFromYML(file, "info", "") + } // Get the parsed contents doc, err := getDoc(ctx, d, path) diff --git a/openapi/table_openapi_path.go b/openapi/table_openapi_path.go index 1836017..aa22687 100644 --- a/openapi/table_openapi_path.go +++ b/openapi/table_openapi_path.go @@ -78,7 +78,15 @@ func listOpenAPIPaths(ctx context.Context, d *plugin.QueryData, h *plugin.Hydrat // For each path, scan its arguments for apiPath, item := range doc.Paths { - startLine, endLine := findBlockLines(file, "paths", apiPath) + + // fetch start and end line for each path + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "paths", apiPath) + } else { + startLine, endLine = findBlockLinesFromYML(file, "paths", apiPath) + } + for _, op := range OperationTypes { operation := getOperationInfoByType(op, item) diff --git a/openapi/table_openapi_path_request_body.go b/openapi/table_openapi_path_request_body.go index 78d4d72..a681173 100644 --- a/openapi/table_openapi_path_request_body.go +++ b/openapi/table_openapi_path_request_body.go @@ -80,7 +80,15 @@ func listOpenAPIPathRequestBodies(ctx context.Context, d *plugin.QueryData, h *p if operation.RequestBody == nil { continue } - startLine, endLine := findBlockLines(file, "paths", apiPath, "requestBody") + + // fetch start and end line for each requestBody block present in path + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "paths", apiPath, "requestBody") + } else { + startLine, endLine = findBlockLinesFromYML(file, "paths", apiPath, "requestBody") + } + requestBodyObject := openAPIPathRequestBody{ Path: path, ApiPath: p.Join(apiPath, op), diff --git a/openapi/table_openapi_path_response.go b/openapi/table_openapi_path_response.go index ce8a4af..5a56293 100644 --- a/openapi/table_openapi_path_response.go +++ b/openapi/table_openapi_path_response.go @@ -81,7 +81,15 @@ func listOpenAPIPathResponses(ctx context.Context, d *plugin.QueryData, h *plugi } for responseStatus, response := range operation.Responses { - startLine, endLine := findBlockLines(file, "paths", apiPath, responseStatus) + + // fetch start and end line for each response block present in path + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "paths", apiPath, responseStatus) + } else { + startLine, endLine = findBlockLinesFromYML(file, "paths", apiPath, responseStatus) + } + responseObject := openAPIPathResponse{ Path: path, ApiPath: p.Join(apiPath, op), diff --git a/openapi/table_openapi_server.go b/openapi/table_openapi_server.go index 5ac9fea..c68218f 100644 --- a/openapi/table_openapi_server.go +++ b/openapi/table_openapi_server.go @@ -3,6 +3,7 @@ package openapi import ( "context" "os" + "strings" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" "github.com/turbot/steampipe-plugin-sdk/v5/plugin" @@ -60,7 +61,15 @@ func listOpenAPIServers(ctx context.Context, d *plugin.QueryData, h *plugin.Hydr // For each server, scan its arguments for _, server := range doc.Servers { - startLine, endLine := findBlockLines(file, "servers", server.URL) + + // fetch start and end line for each server + var startLine, endLine int + if strings.HasSuffix(path, "json") { + startLine, endLine = findBlockLinesFromJSON(file, "servers", server.URL) + } else { + startLine, endLine = findBlockLinesFromYML(file, "servers", server.URL) + } + d.StreamListItem(ctx, openAPIServer{path, startLine, endLine, *server}) // Context may get cancelled due to manual cancellation or if the limit has been reached diff --git a/openapi/utils.go b/openapi/utils.go index eb8205d..7aa11a5 100644 --- a/openapi/utils.go +++ b/openapi/utils.go @@ -113,11 +113,16 @@ func getDocUncached(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateD return doc, nil } -func findBlockLines(file *os.File, blockName string, pathName ...string) (int, int) { +// findBlockLinesFromJSON locates the start and end lines of a specific block or nested element within a block. +// The file should contain structured data (e.g., JSON) and this function expects to search for blocks with specific names. +func findBlockLinesFromJSON(file *os.File, blockName string, pathName ...string) (int, int) { var currentLine, startLine, endLine int var bracketCounter int - inBlock, inPath, inResponseStatus, inRequestBody, inServer := false, false, false, false, false + // These boolean flags indicate which part of the structured data we're currently processing. + inBlock, inPath, inResponseStatus, inRequestBody, inServer, inComponent := false, false, false, false, false, false + + // Move the file pointer to the start of the file. file.Seek(0, 0) scanner := bufio.NewScanner(file) @@ -126,36 +131,50 @@ func findBlockLines(file *os.File, blockName string, pathName ...string) (int, i line := scanner.Text() trimmedLine := strings.TrimSpace(line) - // Detect start of desired block or path or response - if (!inBlock && trimmedLine == fmt.Sprintf(`"%s": {`, blockName)) || !inBlock && trimmedLine == fmt.Sprintf(`"%s": [`, blockName) { + // Detect the start of the desired block, path, response, etc. + // Depending on the blockName and provided pathName, different conditions are checked. + + // Generic block detection + if !inBlock && ((trimmedLine == fmt.Sprintf(`"%s": {`, blockName) || trimmedLine == fmt.Sprintf(`"%s": [`, blockName)) || trimmedLine == fmt.Sprintf(`%s:`, blockName)) { inBlock = true bracketCounter = 1 startLine = currentLine continue - } else if inBlock && !inServer && strings.Contains(trimmedLine, fmt.Sprintf(`"url": "%s"`, pathName[0])) { + } else if inBlock && blockName == "components" && trimmedLine == fmt.Sprintf(`"%s": {`, pathName[0]) { + // Different component block detection within the "components" block + inComponent = true + bracketCounter = 1 + startLine = currentLine + continue + } else if inBlock && blockName == "servers" && strings.Contains(trimmedLine, fmt.Sprintf(`"url": "%s"`, pathName[0])) { + // Server detection within the "servers" block inServer = true bracketCounter = 1 startLine = currentLine - 1 continue - } else if inBlock && blockName == "paths" && !inPath && len(pathName) > 0 && trimmedLine == fmt.Sprintf(`"%s": {`, pathName[0]) { + } else if inBlock && blockName == "paths" && len(pathName) > 0 && trimmedLine == fmt.Sprintf(`"%s": {`, pathName[0]) { + // Path detection within the "paths" block inPath = true bracketCounter = 1 startLine = currentLine continue - } else if inPath && !inRequestBody && len(pathName) > 1 && pathName[1] == "requestBody" && trimmedLine == `"requestBody": {` { + } else if inPath && len(pathName) > 1 && pathName[1] == "requestBody" && trimmedLine == `"requestBody": {` { + // Request body detection within a path inRequestBody = true bracketCounter = 1 startLine = currentLine continue - } else if inPath && !inResponseStatus && len(pathName) > 1 && trimmedLine == fmt.Sprintf(`"%s": {`, pathName[1]) { + } else if inPath && len(pathName) > 1 && trimmedLine == fmt.Sprintf(`"%s": {`, pathName[1]) { + // Response status detection within a path inResponseStatus = true bracketCounter = 1 startLine = currentLine continue } - // If we're in a block/path/responseStatus, track the brackets to find its end - if (inBlock && !inServer) || (inBlock && !inPath) || (inPath && !inResponseStatus) || (inPath && !inRequestBody) || inResponseStatus || inRequestBody { + // If we are within a block, we need to track the opening and closing brackets + // to determine where the block ends. + if (inBlock && !inServer) || (inBlock && !inComponent) || (inBlock && !inPath) || (inPath && !inResponseStatus) || (inPath && !inRequestBody) { bracketCounter += strings.Count(line, "{") bracketCounter -= strings.Count(line, "}") @@ -167,9 +186,89 @@ func findBlockLines(file *os.File, blockName string, pathName ...string) (int, i } if startLine != 0 && endLine == 0 { - // If the end of the block was not found, reset the start to indicate this block doesn't exist + // If we found the start but not the end, reset the start to indicate the block doesn't exist in entirety. startLine = 0 } return startLine, endLine } + +// findBlockLinesFromJSON locates the start and end lines of a specific block or nested element within a block. +// The file should contain structured data (e.g., YML/YAML) and this function expects to search for blocks with specific names. +func findBlockLinesFromYML(file *os.File, blockName string, pathName ...string) (int, int) { + var currentLine, startLine, endLine, currentIndentLevel int + var blockIndentLevel, pathIndentLevel, serverIndentLevel, requestBodyIndentLevel, componentIndentLevel, responseIndentLevel = -1, -1, -1, -1, -1, -1 + + inBlock, inPath, inServer, inRequestBody, inComponent, inResponseStatus := false, false, false, false, false, false + + file.Seek(0, 0) + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + currentLine++ + line := scanner.Text() + + // Determine the current indentation level by counting leading spaces. + currentIndentLevel = len(line) - len(strings.TrimSpace(line)) + + // Detect the start of the desired block. + if !inBlock && strings.HasPrefix(strings.TrimSpace(line), blockName+":") { + inBlock = true + startLine = currentLine + blockIndentLevel = currentIndentLevel + continue + } else if inBlock && blockName == "components" && len(pathName) > 0 && strings.HasPrefix(strings.TrimSpace(line), fmt.Sprintf("%s:", pathName[0])) { + inComponent = true + startLine = currentLine + componentIndentLevel = currentIndentLevel + continue + } else if inBlock && blockName == "paths" && len(pathName) > 0 && strings.HasPrefix(strings.TrimSpace(line), fmt.Sprintf("%s:", pathName[0])) { + inPath = true + startLine = currentLine + pathIndentLevel = currentIndentLevel + continue + } else if inPath && len(pathName) > 1 && pathName[1] == "requestBody" && strings.HasPrefix(strings.TrimSpace(line), "requestBody:") { + inRequestBody = true + startLine = currentLine + requestBodyIndentLevel = currentIndentLevel + continue + } else if inPath && len(pathName) > 1 && strings.HasPrefix(strings.TrimSpace(line), fmt.Sprintf(`"%s":`, pathName[1])) { + inResponseStatus = true + startLine = currentLine + responseIndentLevel = currentIndentLevel + continue + } else if inBlock && blockName == "servers" && len(pathName) > 0 && strings.Contains(strings.TrimSpace(line), fmt.Sprintf(`url: "%s"`, pathName[0])) { + inServer = true + startLine = currentLine + serverIndentLevel = currentIndentLevel + continue + } + + // // If we are within a block, we need to track the closing + if inComponent && currentIndentLevel <= componentIndentLevel { + endLine = currentLine - 1 + break + } else if inPath && currentIndentLevel <= pathIndentLevel { + endLine = currentLine - 1 + break + } else if inServer && currentIndentLevel <= serverIndentLevel { + endLine = currentLine - 1 + break + } else if inRequestBody && currentIndentLevel <= requestBodyIndentLevel { + endLine = currentLine - 1 + break + } else if inResponseStatus && currentIndentLevel <= responseIndentLevel { + endLine = currentLine - 1 + break + } else if inBlock && currentIndentLevel <= blockIndentLevel { + endLine = currentLine - 1 + break + } + } + + if startLine != 0 && endLine == 0 { + endLine = currentLine // Consider the end of the file as the end of the block or path + } + + return startLine, endLine +}