package eia import ( "context" "errors" "fmt" "net/http" "reflect" "slices" "strconv" "strings" "time" eiaapi "gitea.libretechconsulting.com/rmcguire/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 StrNamedParams map[string]reflect.Value // Parameter names by string name from eiaapi mappings RequestEditorFns []eiaapi.RequestEditorFn // Optional request editor functions } var yearStringVal = reflect.ValueOf(eiaapi.Route1( strconv.Itoa(time.Now().AddDate(-1, 0, 0).Year()), )) // By default replace all routes with a string year // To be more specific, set NameParams, which will overwrite // NameContainsParams var defaultMethodSubs = MethodSubs{ StrNamedParams: map[string]reflect.Value{ "route1": yearStringVal, "route2": yearStringVal, }, StrTypedParams: map[string]reflect.Value{ "Route1": yearStringVal, "Route2": yearStringVal, }, } //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 } // Retrieve information for a named Route (e.g. GetAeoV2Route1) // 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, ) { eiaClient := reflect.ValueOf(client) // Get the method for describing this facet method := eiaClient.MethodByName(route) if !method.IsValid() { return nil, nil, fmt.Errorf("method %s not found", route) } parser, err := getParser(route) if err != nil { return nil, nil, err } args := prepMethodArgs(method, route, subs) results := method.Call(args) resp, err := getResponse(results) if err != nil { return nil, nil, err } result, err := ParseResponse(&ParseOpts{ Parser: parser, Resp: resp, JsonOnly: true, }) if err != nil { return nil, nil, err } // First, see if we have a Final Route frr, err := getFRR(result) if err == nil && frr.Response != nil { return frr.Response, nil, nil } // Then, see if we have an intermediate route rr, err := getRR(result) if err == nil && rr.Response != nil { return nil, rr.Response, nil } 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 } // 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 parser, err := getParser(methodName) if err != nil { return nil, err } results := method.Call(args) resp, err := getResponse(results) if err != nil { return nil, err } result, err := ParseResponse(&ParseOpts{ Parser: parser, Resp: resp, JsonOnly: true, }) if err != nil { return nil, err } facetDetails, ok := result.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 parser, err := getParser(route) if err != nil { return nil, err } // 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 } result, err := ParseResponse(&ParseOpts{ Parser: parser, Resp: resp, JsonOnly: true, }) if err != nil { return nil, err } facetOptions, ok := result.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 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 { for name, val := range subs.StrTypedParams { if paramType.Type == name { args = append(args, val) goto next } } for name, val := range subs.StrNamedParams { if paramType.Name == 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 } // 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) { var result reflect.Value results := opts.Parser.Call([]reflect.Value{reflect.ValueOf(opts.Resp)}) 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()) } 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") } if !field.IsValid() { return result, errors.New("invalid response container") } return field, nil } 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 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 } 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 }