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 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{ TypedParams: map[reflect.Type]reflect.Value{ reflect.TypeOf((*eiaapi.Route1)(nil)): reflect.ValueOf( 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) } // Populate facet param anywhere in func call subs.TypedParams[reflect.TypeOf((*eiaapi.FacetId)(nil))] = reflect.ValueOf(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[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 } // Retrieve arg from generated arg mapping due to use of // type aliases (e.g. type Route1 = string) rather than use of // discrete types (e.g. type Route1 string) in generated code funcArgs, ok := eiaapi.FunctionParams[name] if ok { if i > len(funcArgs) { // WARN: This indicates a generator issues } else { argType = funcArgs[i] } } else { // WARN: This indicates a generator issues } // 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 }