2024-11-26 15:54:55 +00:00
|
|
|
package eia
|
|
|
|
|
|
|
|
import (
|
2024-11-26 22:12:39 +00:00
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2024-11-26 15:54:55 +00:00
|
|
|
"reflect"
|
|
|
|
"slices"
|
2024-11-26 22:12:39 +00:00
|
|
|
"strconv"
|
2024-11-26 15:54:55 +00:00
|
|
|
"strings"
|
2024-11-26 22:12:39 +00:00
|
|
|
"time"
|
2024-11-26 15:54:55 +00:00
|
|
|
|
2024-12-17 21:02:58 +00:00
|
|
|
eiaapi "gitea.libretechconsulting.com/rmcguire/eia-api-go/api"
|
2024-11-26 15:54:55 +00:00
|
|
|
)
|
|
|
|
|
2024-11-26 22:12:39 +00:00
|
|
|
// 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
|
2024-11-27 01:18:10 +00:00
|
|
|
StrTypedParams map[string]reflect.Value // Parameter types by string name from eiaapi mappings
|
2024-11-27 18:52:15 +00:00
|
|
|
StrNamedParams map[string]reflect.Value // Parameter names by string name from eiaapi mappings
|
2024-11-26 22:12:39 +00:00
|
|
|
RequestEditorFns []eiaapi.RequestEditorFn // Optional request editor functions
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
var yearStringVal = reflect.ValueOf(eiaapi.Route1(
|
|
|
|
strconv.Itoa(time.Now().AddDate(-1, 0, 0).Year()),
|
|
|
|
))
|
|
|
|
|
2024-11-26 22:12:39 +00:00
|
|
|
// By default replace all routes with a string year
|
|
|
|
// To be more specific, set NameParams, which will overwrite
|
|
|
|
// NameContainsParams
|
|
|
|
var defaultMethodSubs = MethodSubs{
|
2024-11-27 18:52:15 +00:00
|
|
|
StrNamedParams: map[string]reflect.Value{
|
|
|
|
"route1": yearStringVal,
|
|
|
|
"route2": yearStringVal,
|
|
|
|
},
|
2024-11-27 01:18:10 +00:00
|
|
|
StrTypedParams: map[string]reflect.Value{
|
2024-11-27 18:52:15 +00:00
|
|
|
"Route1": yearStringVal,
|
|
|
|
"Route2": yearStringVal,
|
2024-11-26 22:12:39 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-12-04 21:01:07 +00:00
|
|
|
//go:generate stringer -type=RouteType
|
|
|
|
type RouteType uint8
|
|
|
|
|
|
|
|
const (
|
|
|
|
RouteTypeRoutes RouteType = iota
|
|
|
|
RouteTypeFinal
|
|
|
|
RouteTypeFacets
|
|
|
|
RouteTypeNotFound
|
|
|
|
RouteTypeError
|
|
|
|
)
|
|
|
|
|
|
|
|
// Checks the route to see if it returns a facet, a list of routes,
|
|
|
|
// or a final route
|
|
|
|
func (client *Client) GetRouteType(ctx context.Context, route string, subs *MethodSubs) (RouteType, error) {
|
|
|
|
if facets, err := client.GetFacets(ctx, route, subs); err == nil && facets != nil {
|
|
|
|
return RouteTypeFacets, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
finalRoute, routes, err := client.GetRoutesOrFinalRoute(ctx, route, subs)
|
|
|
|
if finalRoute != nil {
|
|
|
|
return RouteTypeFinal, nil
|
|
|
|
} else if routes != nil {
|
|
|
|
return RouteTypeRoutes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return RouteTypeNotFound, err
|
|
|
|
}
|
|
|
|
|
2024-11-27 21:06:30 +00:00
|
|
|
// Retrieve information for a named Route (e.g. GetAeoV2Route1)
|
2024-11-29 21:58:12 +00:00
|
|
|
// Returns a *eiaapi.Routes if this is not a final route, otherwise returns
|
|
|
|
// a final route response
|
|
|
|
func (client *Client) GetRoutesOrFinalRoute(ctx context.Context, route string, subs *MethodSubs) (
|
|
|
|
*eiaapi.FinalRoute, *eiaapi.Routes, error,
|
2024-11-27 02:34:51 +00:00
|
|
|
) {
|
|
|
|
eiaClient := reflect.ValueOf(client)
|
|
|
|
|
|
|
|
// Get the method for describing this facet
|
|
|
|
method := eiaClient.MethodByName(route)
|
|
|
|
if !method.IsValid() {
|
2024-11-29 21:58:12 +00:00
|
|
|
return nil, nil, fmt.Errorf("method %s not found", route)
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
parser, err := getParser(route)
|
|
|
|
if err != nil {
|
2024-11-29 21:58:12 +00:00
|
|
|
return nil, nil, err
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
args := prepMethodArgs(method, route, subs)
|
2024-11-27 18:52:15 +00:00
|
|
|
|
2024-11-27 02:34:51 +00:00
|
|
|
results := method.Call(args)
|
|
|
|
resp, err := getResponse(results)
|
|
|
|
if err != nil {
|
2024-11-29 21:58:12 +00:00
|
|
|
return nil, nil, err
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
result, err := ParseResponse(&ParseOpts{
|
|
|
|
Parser: parser,
|
|
|
|
Resp: resp,
|
2024-11-29 21:58:12 +00:00
|
|
|
JsonOnly: true,
|
2024-11-27 18:52:15 +00:00
|
|
|
})
|
2024-11-27 02:34:51 +00:00
|
|
|
if err != nil {
|
2024-11-29 21:58:12 +00:00
|
|
|
return nil, nil, err
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
2024-11-29 21:58:12 +00:00
|
|
|
// First, see if we have a Final Route
|
|
|
|
frr, err := getFRR(result)
|
|
|
|
if err == nil && frr.Response != nil {
|
|
|
|
return frr.Response, nil, nil
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
2024-11-29 21:58:12 +00:00
|
|
|
// Then, see if we have an intermediate route
|
|
|
|
rr, err := getRR(result)
|
|
|
|
if err == nil && rr.Response != nil {
|
|
|
|
return nil, rr.Response, nil
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
2024-11-29 21:58:12 +00:00
|
|
|
return nil, nil, errors.New("No route or final route fount in response")
|
|
|
|
}
|
|
|
|
|
|
|
|
func getRR(json200 reflect.Value) (*eiaapi.RouteResponse, error) {
|
|
|
|
rr, ok := json200.Interface().(*eiaapi.RouteResponse)
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("response does not contain route response container")
|
|
|
|
}
|
|
|
|
|
|
|
|
if rr == nil {
|
|
|
|
return nil, errors.New("no route response found")
|
|
|
|
}
|
|
|
|
|
|
|
|
return rr, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFRR(json200 reflect.Value) (*eiaapi.FinalRouteResponse, error) {
|
|
|
|
frr, ok := json200.Interface().(*eiaapi.FinalRouteResponse)
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("response does not contain final route response")
|
|
|
|
}
|
|
|
|
|
|
|
|
if frr == nil {
|
|
|
|
return nil, errors.New("no final route response found")
|
|
|
|
}
|
|
|
|
|
|
|
|
return frr, nil
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
2024-11-26 22:12:39 +00:00
|
|
|
// Given an API route and a facet ID, retrieve information about the facet
|
2024-11-27 02:34:51 +00:00
|
|
|
func (client *Client) GetFacet(ctx context.Context, route string, facet string, subs *MethodSubs) (
|
|
|
|
*eiaapi.FacetDetails, error,
|
|
|
|
) {
|
2024-11-26 22:12:39 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-11-27 01:18:10 +00:00
|
|
|
subs.StrTypedParams["FacetId"] = reflect.ValueOf(eiaapi.FacetId(facet))
|
2024-11-26 22:12:39 +00:00
|
|
|
|
|
|
|
args := prepMethodArgs(method, methodName, subs)
|
|
|
|
|
|
|
|
// Prepare a parser func for our facet response
|
2024-11-27 02:34:51 +00:00
|
|
|
parser, err := getParser(methodName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2024-11-26 22:12:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
results := method.Call(args)
|
|
|
|
resp, err := getResponse(results)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
result, err := ParseResponse(&ParseOpts{
|
|
|
|
Parser: parser,
|
|
|
|
Resp: resp,
|
|
|
|
JsonOnly: true,
|
|
|
|
})
|
2024-11-27 02:34:51 +00:00
|
|
|
if err != nil {
|
2024-11-26 22:12:39 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-11-27 02:34:51 +00:00
|
|
|
facetDetails, ok := result.Interface().(*eiaapi.FacetDetailsContainer)
|
2024-11-26 22:12:39 +00:00
|
|
|
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
|
2024-11-27 02:34:51 +00:00
|
|
|
func (client *Client) GetFacets(ctx context.Context, route string, subs *MethodSubs) (
|
|
|
|
*eiaapi.FacetOptionList, error,
|
|
|
|
) {
|
2024-11-26 22:12:39 +00:00
|
|
|
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
|
2024-11-27 02:34:51 +00:00
|
|
|
parser, err := getParser(route)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2024-11-26 22:12:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
result, err := ParseResponse(&ParseOpts{
|
|
|
|
Parser: parser,
|
|
|
|
Resp: resp,
|
|
|
|
JsonOnly: true,
|
|
|
|
})
|
2024-11-27 02:34:51 +00:00
|
|
|
if err != nil {
|
2024-11-26 22:12:39 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-11-27 02:34:51 +00:00
|
|
|
facetOptions, ok := result.Interface().(*eiaapi.FacetOptionListContainer)
|
2024-11-26 22:12:39 +00:00
|
|
|
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
|
|
|
|
|
2024-11-27 01:18:10 +00:00
|
|
|
subs.TypedParams = map[reflect.Type]reflect.Value{
|
|
|
|
reflect.TypeOf((*context.Context)(nil)).Elem(): reflect.ValueOf(ctx),
|
|
|
|
}
|
2024-11-26 22:12:39 +00:00
|
|
|
|
|
|
|
return &subs
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return a list of API routes with a given list of
|
|
|
|
// function suffixes
|
|
|
|
func GetRoutes(suffixes ...string) []string {
|
2024-11-26 15:54:55 +00:00
|
|
|
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)
|
2024-11-26 22:12:39 +00:00
|
|
|
if len(suffixes) < 1 {
|
2024-11-26 15:54:55 +00:00
|
|
|
routes = append(routes, method.Name)
|
2024-11-26 22:12:39 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, suffix := range suffixes {
|
|
|
|
if strings.HasSuffix(method.Name, suffix) {
|
|
|
|
routes = append(routes, method.Name)
|
|
|
|
break
|
|
|
|
}
|
2024-11-26 15:54:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return slices.Clip(routes)
|
|
|
|
}
|
2024-11-26 22:12:39 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
// Perform type lookups by string match on either the name of the
|
|
|
|
// parameter, or the type of the parameter, as a string value
|
|
|
|
if paramType := eiaapi.GetFuncParamType(name, i); paramType != nil {
|
2024-11-27 01:18:10 +00:00
|
|
|
for name, val := range subs.StrTypedParams {
|
2024-11-27 18:52:15 +00:00
|
|
|
if paramType.Type == name {
|
|
|
|
args = append(args, val)
|
|
|
|
goto next
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for name, val := range subs.StrNamedParams {
|
|
|
|
if paramType.Name == name {
|
2024-11-27 01:18:10 +00:00
|
|
|
args = append(args, val)
|
|
|
|
goto next
|
|
|
|
}
|
2024-11-26 22:12:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
// If jsonOnly is set, only the parsed value will be returned in a field called JSON200
|
|
|
|
// If there are unmarshaling issues due to code generated from the crappy swagger spec,
|
|
|
|
// you may have to manually unmarshal the "Body" field into the correct type. Setting jsonOnly
|
|
|
|
// to false will return the entire thing rather than just the JSON200 field.
|
|
|
|
type ParseOpts struct {
|
|
|
|
Parser reflect.Value
|
|
|
|
Resp *http.Response
|
|
|
|
JsonOnly bool // Used when parser can correctly unmarshal into type
|
|
|
|
BodyOnly bool // Used when API response does not contain openapi spec suggested type
|
|
|
|
}
|
|
|
|
|
|
|
|
func ParseResponse(opts *ParseOpts) (reflect.Value, error) {
|
2024-11-27 02:34:51 +00:00
|
|
|
var result reflect.Value
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
results := opts.Parser.Call([]reflect.Value{reflect.ValueOf(opts.Resp)})
|
2024-11-27 02:34:51 +00:00
|
|
|
if len(results) != 2 {
|
|
|
|
return result, errors.New("unexpected response while parsing response")
|
|
|
|
}
|
|
|
|
if err := checkCallErr(results[1]); err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
result = results[0]
|
|
|
|
if result.Kind() == reflect.Ptr {
|
|
|
|
result = result.Elem()
|
|
|
|
}
|
|
|
|
|
|
|
|
if result.Kind() != reflect.Struct {
|
|
|
|
return result, fmt.Errorf("unexpected parse result kind %s", result.Kind().String())
|
|
|
|
}
|
|
|
|
|
2024-11-27 18:52:15 +00:00
|
|
|
if !opts.JsonOnly && !opts.BodyOnly {
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var field reflect.Value
|
|
|
|
|
|
|
|
if opts.JsonOnly {
|
|
|
|
field = result.FieldByName("JSON200")
|
|
|
|
} else if opts.BodyOnly {
|
|
|
|
field = result.FieldByName("Body")
|
|
|
|
}
|
|
|
|
|
2024-11-27 02:34:51 +00:00
|
|
|
if !field.IsValid() {
|
2024-11-27 18:52:15 +00:00
|
|
|
return result, errors.New("invalid response container")
|
2024-11-27 02:34:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return field, nil
|
|
|
|
}
|
|
|
|
|
2024-11-26 22:12:39 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-11-27 02:34:51 +00:00
|
|
|
func getParser(forMethod string) (reflect.Value, error) {
|
|
|
|
parserFunc, exists := eiaapi.ParseFunctionsMap[fmt.Sprintf("Parse%sResponse", forMethod)]
|
|
|
|
if !exists {
|
|
|
|
return reflect.Value{}, fmt.Errorf("parser func for %s not found", forMethod)
|
|
|
|
}
|
|
|
|
|
|
|
|
parser := reflect.ValueOf(parserFunc)
|
|
|
|
if !parser.IsValid() {
|
|
|
|
return reflect.Value{}, errors.New("unable to find parser for facet response")
|
|
|
|
}
|
|
|
|
|
|
|
|
return parser, nil
|
|
|
|
}
|
|
|
|
|
2024-11-26 22:12:39 +00:00
|
|
|
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
|
|
|
|
}
|