Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): Make ParseResult use generic Record type #1321

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 106 additions & 8 deletions common/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,40 @@ import (
)

type (
// NextPageFunc extracts the next page token/URL from the response body.
NextPageFunc func(*ajson.Node) (string, error)
RecordsFunc func(*ajson.Node) ([]map[string]any, error)

// RecordsFunc extracts a list of records as map[string]any from the response body.
RecordsFunc func(*ajson.Node) ([]map[string]any, error)
// NodeRecordsFunc extracts a list of records as ajson.Node from the response body.
NodeRecordsFunc func(*ajson.Node) ([]*ajson.Node, error)

// RawReadRecordFunc processes a raw record, applying additional transformations if needed.
// There is one common example of flattening nested fields provided by RecordFlattener.
RawReadRecordFunc func(node *ajson.Node) (map[string]any, error)
// MarshalFunc converts a list of map[string]any records into ReadResultRow format.
MarshalFunc func(records []map[string]any, fields []string) ([]ReadResultRow, error)
// NodeMarshalFunc converts a list of ajson.Node records into ReadResultRow format.
NodeMarshalFunc func(records []*ajson.Node, fields []string) ([]ReadResultRow, error)
)

// RawReadRecord defines the types of records that ParseResult can process.
// It determines which callback function is used for parsing.
type RawReadRecord interface {
map[string]any | *ajson.Node
}

// ParseResult parses the response from a provider into a ReadResult. A 2xx return type is assumed.
// The sizeFunc returns the total number of records in the response.
// The recordsFunc returns the records in the response.
// The nextPageFunc returns the URL for the next page of results.
// The marshalFunc is used to structure the data into an array of ReadResultRows.
// The fields are used to populate ReadResultRow.Fields.
func ParseResult(
func ParseResult[R RawReadRecord](
resp *JSONHTTPResponse,
recordsFunc func(*ajson.Node) ([]map[string]any, error),
recordsFunc func(*ajson.Node) ([]R, error),
nextPageFunc func(*ajson.Node) (string, error),
marshalFunc func([]map[string]any, []string) ([]ReadResultRow, error),
marshalFunc func([]R, []string) ([]ReadResultRow, error),
fields datautils.Set[string],
) (*ReadResult, error) {
body, ok := resp.Body()
Expand Down Expand Up @@ -73,11 +92,11 @@ func ParseResult(

// ExtractLowercaseFieldsFromRaw returns a map of fields from a record.
// The fields are all returned in lowercase.
func ExtractLowercaseFieldsFromRaw(fields []string, record map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(fields))
func ExtractLowercaseFieldsFromRaw(fields []string, record map[string]any) map[string]any {
out := make(map[string]any, len(fields))

// Modify all record keys to lowercase
lowercaseRecord := make(map[string]interface{}, len(record))
lowercaseRecord := make(map[string]any, len(record))
for key, value := range record {
lowercaseRecord[strings.ToLower(key)] = value
}
Expand Down Expand Up @@ -107,7 +126,35 @@ func GetMarshaledData(records []map[string]any, fields []string) ([]ReadResultRo
return data, nil
}

func GetRecordsUnderJSONPath(jsonPath string, nestedPath ...string) RecordsFunc {
// MakeMarshaledDataFunc produces ReadResultRow where raw record differs from the fields.
// This usually includes a set of actions to preprocess, usually to flatten the raw record and then extract
// fields requested by the user.
func MakeMarshaledDataFunc(nodeRecordFunc RawReadRecordFunc) NodeMarshalFunc {
return func(records []*ajson.Node, fields []string) ([]ReadResultRow, error) {
data := make([]ReadResultRow, len(records))

for index, nodeRecord := range records {
raw, err := jsonquery.Convertor.ObjectToMap(nodeRecord)
if err != nil {
return nil, err
}

record, err := nodeRecordFunc(nodeRecord)
if err != nil {
return nil, err
}

data[index] = ReadResultRow{
Fields: ExtractLowercaseFieldsFromRaw(fields, record),
Raw: raw,
}
}

return data, nil
}
}

func GetRecordsUnderJSONPath(jsonPath string, nestedPath ...string) func(*ajson.Node) ([]map[string]any, error) {
return getRecords(false, jsonPath, nestedPath...)
}

Expand Down Expand Up @@ -135,3 +182,54 @@ func getRecords(optional bool, jsonPath string, nestedPath ...string) RecordsFun
return jsonquery.Convertor.ArrayToMap(arr)
}
}

// RecordFlattener returns a procedure which copies fields of a nested object to the top level.
//
// Ex: Every object has special field "attributes" which holds all the object specific fields.
// Therefore, nested "attributes" will be removed and fields will be moved to the top level of the object.
//
// Example accounts(shortened response):
//
// "data": [
// {
// "type": "",
// "id": "",
// "attributes": {
// "test_account": false,
// "contact_information": {},
// "locale": ""
// },
// "links": {}
// }
// ],
//
// The resulting fields for the above will be: [ type, id, test_account, contact_information, locale, links ].
func RecordFlattener(nestedKey string) RawReadRecordFunc {
return func(node *ajson.Node) (map[string]any, error) {
attributes, err := jsonquery.New(node).ObjectOptional(nestedKey)
if err != nil {
return nil, err
}

root, err := jsonquery.Convertor.ObjectToMap(node)
if err != nil {
return nil, err
}

nested, err := jsonquery.Convertor.ObjectToMap(attributes)
if err != nil {
return nil, err
}

// Nested object will be removed.
delete(root, nestedKey)

// Fields from attributes are moved to the top level.
for key, value := range nested {
root[key] = value
}

// Root level has adopted fields from nested object.
return root, nil
}
}