Skip to content

Commit

Permalink
feat(common): Make ParseResult use generic Record type
Browse files Browse the repository at this point in the history
  • Loading branch information
Cobalt0s committed Feb 19, 2025
1 parent 203c233 commit 247223c
Showing 1 changed file with 106 additions and 8 deletions.
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
}
}

0 comments on commit 247223c

Please sign in to comment.