3

I am trying to achieve attribute selection on a Rest Resource according to query parameters. API client will provide a query parameter called fields. Server will return only attributes of the resource mentioned in the query string. Server should return different Partial representation of the Resource according to query parameter. Here are some example requests.

GET /api/person/42/?fields=id,createdAt
GET /api/person/42/?fields=address,account
GET /api/person/42/?fields=id,priority,address.city

I tried to go map[string]any route but it did not go well. I am using MongoDB. When I decode mongo document into map[string]any field names and types are not matching. Therefore I am trying to create a new struct on the fly.

Here is my attempt:

func main() {
    query, _ := url.ParseQuery("fields=id,priority,address.city")
    fields := strings.Split(query.Get("fields"), ",") // TODO: extractFields
    person := getPerson() // Returns a Person Struct 
    personish := PartialStruct(person, fields)
    marshalled, _ := json.Marshal(personish) // TODO: err
    fmt.Println(string(marshalled))
}

func PartialStruct(original any, fields []string) any {
    // Is there any alternative to reflect ?
    originalType := reflect.TypeOf(original)
    partialFields := make([]reflect.StructField, 0)
    for _, field := range reflect.VisibleFields(originalType) {
        queryName := field.Tag.Get("json") // TODO: extractQueryName
        if slices.Contains(fields, queryName) {
            partialFields = append(partialFields, field)
        }
    }
    partialType := reflect.StructOf(partialFields)
    // Is there any alternative to Marshal/Unmarshal?
    partial := reflect.New(partialType).Interface()
    marshalled, _ := json.Marshal(original) // TODO: err
    _ = json.Unmarshal(marshalled, partial) // TODO: err
    return partial
}

Here is a runnable example https://go.dev/play/p/Egomxe5NjEc

Resources are modelled as nested structs. Nested fields will be denoted by a "." dot in the query string.

How can I improve PartialStruct to handle nested fields such as address.city?

I am willing to change my direction if there are better ways.

4
  • 1
    Have you tried using "omitempty" tags, and using pointers for strings, ints, etc? Commented Jan 4, 2023 at 1:48
  • How are you connecting to MongoDB, mongo-go-driver? Commented Jan 5, 2023 at 22:27
  • yes using mongo-go-driver Commented Jan 5, 2023 at 22:49
  • Check out the Jeffail/gabs pkg here, stackoverflow.com/questions/1737171. Commented Jan 5, 2023 at 23:32

2 Answers 2

0

look at the third party libraries:graphql

An example I wrote that may help you:

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/graphql-go/graphql"
)

func main() {
    // Schema
    fields := graphql.Fields{
        "id": &graphql.Field{
            Type: graphql.ID,
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                return 111, nil
            },
        },
        "priority": &graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                return "admin", nil
            },
        },
        "address": &graphql.Field{
            Type: graphql.NewObject(graphql.ObjectConfig{
                Name: "address",
                Fields: graphql.Fields{
                    "city": &graphql.Field{
                        Type: graphql.String,
                    },
                    "country":  &graphql.Field{
                        Type: graphql.String,
                    },
                },
            }),
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                return map[string]string{
                    "city":"New York",
                    "country": "us",
                }, nil
            },
        },
    }
    rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
    schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)}
    schema, err := graphql.NewSchema(schemaConfig)
    if err != nil {
        log.Fatalf("failed to create new schema, error: %v", err)
    }

    // Query
    query := `
        {
            id,
            address {
                city,country
            },
            priority
        }
    `
    params := graphql.Params{Schema: schema, RequestString: query}
    r := graphql.Do(params)
    if len(r.Errors) > 0 {
        log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
    }
    rJSON, _ := json.Marshal(r)
    fmt.Printf("%s \n", rJSON)
}

Here is a runnable example https://go.dev/play/p/pHH2iBzCLT-

Sign up to request clarification or add additional context in comments.

3 Comments

Suggesting a completely different protocol and a massive dependency just to help parse a query parameter is completely unhelpful.
@Evert, maybe OP wants or needs to see this. My first thought when I saw OP's question was "doesn't GraphQL already solve this?" If the work has already been done, why put an answer down for suggesting that? Also, can you recommend something better?
It's like suggesting to buy a car when your bike has a flat tire.
0

The library you're looking for is json-mask-go which will handle the partial responses post marshalling

Here's an example (also note the change of the address.city to address/city)

https://go.dev/play/p/IelEOjctZ5t

    query, _ := url.ParseQuery("fields=id,priority,address/city")

    person := getPerson()
    marshalled, _ := json.Marshal(person) // TODO: err

    personish, err := jsonmask.Mask([]byte(marshalled), query.Get("fields"))
    if err != nil {
        fmt.Println("Error", err)
        return
    }

    fmt.Println(string(personish))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.