Source Code

Consumir un webservice REST


Tiempo de lectura: 1 minutos

Conectar a una API de tipo REST utilizando OAuth 1.0



FatSecret API

FatSecret es una base de datos nutricional en la que se puede buscar un alimento para saber su composición nutricional, entro otras muchas cosas. Es necesario disponer de una API Key para poder ejecutar la llamada y que devuelva valores.

Necesitaremos los siguientes imports en el programa principal:

import (
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"

	// local
	"webServices/internal/config"
	"webServices/pkg/fatsecret"
)

Pedir el alimento

Empezamos creando una lectura de consola para que el usuario nos indique el alimento a buscar y guardamos la variable global.

func getUserData() {

	fmt.Print("Alimento: ")
	fmt.Scan(&food)
	if food == "" {
		return
	}
}

Montamos un función que realice la conexión a la API, que en realidad simplemente almacena las claves y crea una clase para acceder al cliente. Con la clase fs tenemos acceso a los métodos del cliente.

func callFatsecret(fsApiKey map[string]string) {

	fs, err := fatsecret.Connect(fsApiKey["apikey"], fsApiKey["secret"])
	if err != nil {
		panic(err)
	}

	foods, err := fs.FoodSearch(food)
	if err != nil {
		panic(err)
	}

	fmt.Println(foods[0])
}

Aunque en Go no hay clases, nos devuelve fs que podemos considerar una clase con los métodos fs.FoodSearch y fs.get. Y cuya estructura de datos y métodos es

type FatSecretConn struct {
	apikey string
	secret string
}

func (fs FatSecretConn) FoodSearch
func (fs FatSecretConn) get

FoodSearch

fs.FoodSearch procesa el resultado obtenido al hacer la llamada a la API en fs.get.

func (fs FatSecretConn) FoodSearch(query string) ([]Food, error) {

	// llamada a la PI
	resp, err := fs.get(
		"foods.search",
		map[string]string{"search_expression": query},
	)
	if err != nil {
		return nil, err
	}
	
	// lectura de la respuesta en JSON
	body, err := io.ReadAll(resp)
	if err != nil {
		return nil, err
	}
	defer resp.Close()

	// procesamos la parte de JSON que nos interesa 
	foodresp := FoodSearchResponse{}
	err = json.Unmarshal(body, &foodresp)
	if err != nil {
		return nil, err
	}
	if foodresp.Error != nil {
		return nil, errors.New(foodresp.Error.Message)
	}
	
	// retornamos el array
	return foodresp.Foods.Food, nil
}

get

El método get básicamente monta los parámetros necesarios para hacer el request y devolver el body. Para utilizar OAuth tenemos que crear una firma de los parámetros del request.

func (fs FatSecretConn) get(method string, params map[string]string) (io.ReadCloser, error) {

	reqTime := fmt.Sprintf("%d", time.Now().Unix())
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	m := map[string]string{
		"method":                 method,
		"oauth_consumer_key":     fs.apikey,
		"oauth_nonce":            fmt.Sprintf("%d", r.Int63()),
		"oauth_signature_method": "HMAC-SHA1",
		"oauth_timestamp":        reqTime,
		"oauth_version":          "1.0",
		"format":                 "json",
	}

	// las claves deben estar ordenadas alfabéticamente 

	for k, v := range params {
		m[k] = v
	}
	mk := make([]string, len(m))
	i := 0
	for k := range m {
		mk[i] = k
		i++
	}
	sort.Strings(mk)

	// monta las clase/valor ordenadas para la firma

	sigQueryStr := ""
	for _, k := range mk {
		sigQueryStr += fmt.Sprintf("&%s=%s", k, escape(m[k]))
	}

	// elimina el & inicial

	sigQueryStr = sigQueryStr[1:]

	// monta el string base para firmarlo

	sigBaseStr := fmt.Sprintf("GET&%s&%s", url.QueryEscape(fsurl), escape(sigQueryStr))

	// crea una firma

	mac := hmac.New(sha1.New, []byte(fs.secret+"&"))
	mac.Write([]byte(sigBaseStr))
	sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	// añade la firma al mapa de parámetros

	m["oauth_signature"] = sig
	mk = append(mk, "oauth_signature")

	// reordena las claves

	sort.Strings(mk)
	requrl := fmt.Sprintf("%s?", fsurl)
	reqQuery := ""
	for _, k := range mk {
		reqQuery += fmt.Sprintf("&%s=%s", k, escape(m[k]))
	}

	// elimina el & inicial

	reqQuery = reqQuery[1:]

	// envia el request

	requrl += reqQuery
	resp, err := http.Get(requrl)
	if err != nil {
		return nil, err
	}
	return resp.Body, nil
}

Respuesta

Esta es la respuesta obtenida después de procesar el JSON

{2057 Salmon Generic Per 100g - Calories: 146kcal | Fat: 5.93g | Carbs: 0.00g | Protein: 21.62g}
{2058 Cooked Salmon  Per 100g - Calories: 139kcal | Fat: 4.30g | Carbs: 0.00g | Protein: 23.45g}
{2059 Baked Salmon Per 845g - Calories: 1445kcal | Fat: 63.87g | Carbs: 4.14g | Protein: 202.52g}

El código completo del cliente original se puede encontrar en GitHub.

Referencias
Más artículos




Xavier es un desarrollador senior full stack y opera desde la ciudad mediterránea de Barcelona. Le encantan las tecnologías de software y está convencido que el desarrollo de software es un proceso colaborativo y abierto.
Y es un apasionado de la astronomía y de la fotografía. Lo puedes encontrar en:
Comparte este post