eia-api-go/pkg/eia/eia_reflection.go

294 lines
7.7 KiB
Go

package eia
import (
"context"
"errors"
"fmt"
"net/http"
"reflect"
"slices"
"strconv"
"strings"
"time"
eiaapi "gitea.libretechconsulting.com/50W/eia-api-go/api"
)
// For reflected API requests, dynamically replace certain
// parameters by type
type MethodSubs struct {
TypedParams map[reflect.Type]reflect.Value // Replace field of specific type with value, must be ptr to type
StrTypedParams map[string]reflect.Value // Parameter types by string name from eiaapi mappings
RequestEditorFns []eiaapi.RequestEditorFn // Optional request editor functions
}
// By default replace all routes with a string year
// To be more specific, set NameParams, which will overwrite
// NameContainsParams
var defaultMethodSubs = MethodSubs{
StrTypedParams: map[string]reflect.Value{
"Route1": reflect.ValueOf(eiaapi.Route1(
strconv.Itoa(time.Now().AddDate(-1, 0, 0).Year()),
)),
"Route2": reflect.ValueOf(eiaapi.Route1(
strconv.Itoa(time.Now().AddDate(-1, 0, 0).Year()),
)),
},
}
// Given an API route and a facet ID, retrieve information about the facet
func (client *Client) GetFacet(ctx context.Context, route string, facet string, subs *MethodSubs) (*eiaapi.FacetDetails, error) {
eiaClient := reflect.ValueOf(client)
// Get the method for describing this facet
methodName := fmt.Sprintf("%sFacetId", route)
method := eiaClient.MethodByName(methodName)
if !method.IsValid() {
return nil, fmt.Errorf("method %s not found", methodName)
}
subs.StrTypedParams["FacetId"] = reflect.ValueOf(eiaapi.FacetId(facet))
args := prepMethodArgs(method, methodName, subs)
// Prepare a parser func for our facet response
parserFunc, exists := eiaapi.ParseFunctionsMap[fmt.Sprintf("Parse%sResponse", methodName)]
if !exists {
return nil, fmt.Errorf("parser func for %s not found", route)
}
parser := reflect.ValueOf(parserFunc)
if !parser.IsValid() {
return nil, errors.New("unable to find parser for facet response")
}
results := method.Call(args)
resp, err := getResponse(results)
if err != nil {
return nil, err
}
results = parser.Call([]reflect.Value{reflect.ValueOf(resp)})
if len(results) != 2 {
return nil, errors.New("unexpected response while parsing facet response")
}
if err := checkCallErr(results[1]); err != nil {
return nil, err
}
result := results[0]
if result.Kind() == reflect.Ptr {
result = result.Elem()
}
if result.Kind() != reflect.Struct {
return nil, fmt.Errorf("unexpected parse result kind %s", result.Kind().String())
}
field := result.FieldByName("JSON200")
if !field.IsValid() {
return nil, errors.New("invalid facet data field in response")
}
facetDetails, ok := field.Interface().(*eiaapi.FacetDetailsContainer)
if !ok {
return nil, errors.New("response does not contain facet details")
}
if facetDetails == nil {
return nil, errors.New("no facet details found for facet")
}
return facetDetails.Response, nil
}
// Return a list of facets given a named route
func (client *Client) GetFacets(ctx context.Context, route string, subs *MethodSubs) (*eiaapi.FacetOptionList, error) {
eiaClient := reflect.ValueOf(client)
// Get the method by name
method := eiaClient.MethodByName(route)
if !method.IsValid() {
return nil, fmt.Errorf("method %q not found", route)
}
args := prepMethodArgs(method, route, subs)
// Prepare a parser func for our facet response
parserFunc, exists := eiaapi.ParseFunctionsMap[fmt.Sprintf("Parse%sResponse", route)]
if !exists {
return nil, fmt.Errorf("parser func for %s not found", route)
}
parser := reflect.ValueOf(parserFunc)
if !parser.IsValid() {
return nil, errors.New("unable to find parser for facet response")
}
// Perform the API call
results := method.Call(args)
if len(results) != 2 {
return nil, errors.New("unexpected response from get facet call")
}
// Prepare *http.Response, error
resp, err := getResponse(results)
if err != nil {
return nil, err
}
// Call the parser with our response, then extract the JSON200 response,
// and return the expected FacetOptiionsList from the container
results = parser.Call([]reflect.Value{reflect.ValueOf(resp)})
if len(results) != 2 {
return nil, errors.New("unexpected response while parsing facet response")
}
if err := checkCallErr(results[1]); err != nil {
return nil, err
}
result := results[0]
if result.Kind() == reflect.Ptr {
result = result.Elem()
}
if result.Kind() != reflect.Struct {
return nil, fmt.Errorf("unexpected parse result kind %s", result.Kind().String())
}
field := result.FieldByName("JSON200")
if !field.IsValid() {
return nil, errors.New("invalid facet data field in response")
}
facetOptions, ok := field.Interface().(*eiaapi.FacetOptionListContainer)
if !ok {
return nil, errors.New("response does not contain facet options")
}
if facetOptions == nil {
return nil, errors.New("no facet options found for facet request")
}
return facetOptions.Response, nil
}
// Prepare default substitutions with a provided context
func DefaultMethodSubs(ctx context.Context) *MethodSubs {
subs := defaultMethodSubs
subs.TypedParams = map[reflect.Type]reflect.Value{
reflect.TypeOf((*context.Context)(nil)).Elem(): reflect.ValueOf(ctx),
}
return &subs
}
// Return a list of API routes with a given list of
// function suffixes
func GetRoutes(suffixes ...string) []string {
eiaClientInterface := new(eiaapi.ClientInterface)
t := reflect.TypeOf(eiaClientInterface).Elem()
routes := make([]string, 0, t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
method := t.Method(i)
if len(suffixes) < 1 {
routes = append(routes, method.Name)
continue
}
for _, suffix := range suffixes {
if strings.HasSuffix(method.Name, suffix) {
routes = append(routes, method.Name)
break
}
}
}
return slices.Clip(routes)
}
// Given an API method, replaces any named fields with the provided value
// from the subs map, otherwise uses built-in logic for common fields
// such as a context.Context. Skips request editor funcs
func prepMethodArgs(method reflect.Value, name string, subs *MethodSubs) []reflect.Value {
// Create a slice of reflect.Value for the method's arguments
methodType := method.Type()
args := make([]reflect.Value, 0, methodType.NumIn())
// Populate arguments with zero values for their respective types
for i := 0; i < methodType.NumIn(); i++ {
argType := methodType.In(i)
// Supply provided request editor Fn args if this function
// is varidic and if we're in the lest arg
if methodType.IsVariadic() && i == methodType.NumIn()-1 {
for _, reFn := range subs.RequestEditorFns {
args = append(args, reflect.ValueOf(reFn))
}
break
}
// Perform type lookups by string match
if paramType := eiaapi.GetFuncParamType(name, i); paramType != "" {
for name, val := range subs.StrTypedParams {
if paramType == name {
args = append(args, val)
goto next
}
}
}
// Run through type substitutions
for t, v := range subs.TypedParams {
if argType.Kind() == reflect.Ptr && argType.Elem() == t {
args = append(args, v)
goto next
}
}
// Fall back to less-specific types
for t, v := range subs.TypedParams {
if argType == t {
args = append(args, v)
goto next
}
}
// Zero value of other stuff
args = append(args, reflect.Zero(argType))
next:
}
return args
}
func getResponse(responses []reflect.Value) (*http.Response, error) {
resp, ok := responses[0].Interface().(*http.Response)
if !ok {
return nil, errors.New("no or invalid response received from call")
}
if err := checkCallErr(responses[1]); err != nil {
return nil, err
}
return resp, nil
}
func checkCallErr(val reflect.Value) error {
var err error
var ok bool
if val.IsValid() && !val.IsNil() {
if err, ok = val.Interface().(error); !ok {
return errors.New("unexpected call response")
}
}
return err
}