Skip to content

Commit

Permalink
Add fields support to object adapter
Browse files Browse the repository at this point in the history
Currently the the `fields` query parameter is only supported by the
collection adapter. This patch adds it to the object adapter.

Related: https://issues.redhat.com/browse/MGMT-16115
Signed-off-by: Juan Hernandez <[email protected]>
  • Loading branch information
jhernand committed Nov 7, 2023
1 parent 6b8d641 commit 9784fde
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 45 deletions.
43 changes: 22 additions & 21 deletions internal/service/collection_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,14 @@ func (b *CollectionAdapterBuilder) Build() (result *CollectionAdapter, err error
return
}

// Create the field selector parser:
// Create the projector parser and evaluator:
projectorParser, err := search.NewProjectorParser().
SetLogger(b.logger).
Build()
if err != nil {
err = fmt.Errorf("failed to create field selector parser: %w", err)
return
}

// Create the path evaluator:
pathEvaluator, err := search.NewPathEvaluator().
SetLogger(b.logger).
Build()
Expand Down Expand Up @@ -130,19 +128,21 @@ func (a *CollectionAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"from", r.RemoteAddr,
"url", r.URL.String(),
)
query := r.URL.Query()

// Get the context:
ctx := r.Context()

// Get the query parameters:
query := r.URL.Query()

// Create the request:
request := &CollectionRequest{}

// Check if there is a filter expression, and parse it:
// Check if there is a selector, and parse it:
values, ok := query["filter"]
if ok {
for _, value := range values {
expr, err := a.selectorParser.Parse(value)
selector, err := a.selectorParser.Parse(value)
if err != nil {
a.logger.Error(
"Failed to parse filter expression",
Expand All @@ -157,15 +157,15 @@ func (a *CollectionAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
)
return
}
a.logger.Info(
a.logger.Debug(
"Parsed filter expressions",
slog.String("source", value),
slog.String("parsed", expr.String()),
slog.String("parsed", selector.String()),
)
if request.Filter == nil {
request.Filter = expr
if request.Selector == nil {
request.Selector = selector
} else {
request.Filter.Terms = append(request.Filter.Terms, expr.Terms...)
request.Selector.Terms = append(request.Selector.Terms, selector.Terms...)
}
}
}
Expand All @@ -174,7 +174,7 @@ func (a *CollectionAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
values, ok = query["fields"]
if ok {
for _, value := range values {
selector, err := a.projectorParser.Parse(value)
projector, err := a.projectorParser.Parse(value)
if err != nil {
a.logger.Error(
"Failed to parse field selector",
Expand All @@ -192,14 +192,14 @@ func (a *CollectionAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.logger.Info(
"Parsed field selector",
slog.String("source", value),
slog.Any("parsed", selector),
slog.Any("parsed", projector),
)
request.Selector = append(request.Selector, selector...)
request.Projector = append(request.Projector, projector...)
}
}

// Call the handler:
response, err := a.handler.Get(r.Context(), request)
response, err := a.handler.Get(ctx, request)
if err != nil {
a.logger.Error(
"Failed to get items",
Expand All @@ -214,17 +214,18 @@ func (a *CollectionAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

// If there is a projector apply it:
if request.Selector != nil {
response.Items = data.Map(
response.Items,
items := response.Items
if request.Projector != nil {
items = data.Map(
items,
func(ctx context.Context, item data.Object) (result data.Object, err error) {
result, err = a.projectorEvaluator.Evaluate(ctx, request.Selector, item)
result, err = a.projectorEvaluator.Evaluate(ctx, request.Projector, item)
return
},
)
}

a.sendItems(ctx, w, response.Items)
a.sendItems(ctx, w, items)
}

func (a *CollectionAdapter) sendItems(ctx context.Context, w http.ResponseWriter,
Expand All @@ -236,7 +237,7 @@ func (a *CollectionAdapter) sendItems(ctx context.Context, w http.ResponseWriter
err := writer.Flush()
if err != nil {
slog.Error(
"Faild to flush JSON stream",
"Faild to flush stream",
"error", err.Error(),
)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/service/collection_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ var _ = Describe("Collection adapter", func() {
// Prepare the handler:
body := func(ctx context.Context,
request *CollectionRequest) (response *CollectionResponse, err error) {
Expect(request.Filter).To(Equal(&search.Selector{
Expect(request.Selector).To(Equal(&search.Selector{
Terms: []*search.Term{{
Operator: search.Eq,
Path: []string{
Expand Down Expand Up @@ -119,7 +119,7 @@ var _ = Describe("Collection adapter", func() {
// Prepare the handler:
body := func(ctx context.Context,
request *CollectionRequest) (response *CollectionResponse, err error) {
Expect(request.Filter).To(Equal(&search.Selector{
Expect(request.Selector).To(Equal(&search.Selector{
Terms: []*search.Term{
{
Operator: search.Eq,
Expand Down Expand Up @@ -168,7 +168,7 @@ var _ = Describe("Collection adapter", func() {
// Prepare the handler:
body := func(ctx context.Context,
request *CollectionRequest) (response *CollectionResponse, err error) {
Expect(request.Filter).To(BeNil())
Expect(request.Selector).To(BeNil())
response = &CollectionResponse{
Items: data.Null(),
}
Expand Down
7 changes: 5 additions & 2 deletions internal/service/collection_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import (

// CollectionRequest represents a request for a collection of objects.
type CollectionRequest struct {
Filter *search.Selector
Selector [][]string
// Selector selects the objects to return.
Selector *search.Selector

// Projector is the list of field paths to return.
Projector [][]string
}

// CollectionResponse represents the response to the request to get the list of items of a collection.
Expand Down
4 changes: 2 additions & 2 deletions internal/service/deployment_manager_collection_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ func (h *DeploymentManagerCollectionHandler) Get(ctx context.Context,
items = data.Map(items, h.mapItem)

// Select only the items that satisfy the filter:
if request.Filter != nil {
if request.Selector != nil {
items = data.Select(
items,
func(ctx context.Context, item data.Object) (result bool, err error) {
result, err = h.selectorEvaluator.Evaluate(ctx, request.Filter, item)
result, err = h.selectorEvaluator.Evaluate(ctx, request.Selector, item)
return
},
)
Expand Down
110 changes: 93 additions & 17 deletions internal/service/object_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,29 @@ package service
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"

"github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
"github.com/openshift-kni/oran-o2ims/internal/data"
"github.com/openshift-kni/oran-o2ims/internal/search"
)

type ObjectAdapterBuilder struct {
logger *slog.Logger
handler ObjectHandler
id string
logger *slog.Logger
handler ObjectHandler
idVariable string
}

type ObjectAdapter struct {
logger *slog.Logger
handler ObjectHandler
id string
jsonAPI jsoniter.API
logger *slog.Logger
idVariable string
projectorParser *search.ProjectorParser
projectorEvaluator *search.ProjectorEvaluator
jsonAPI jsoniter.API
handler ObjectHandler
}

func NewObjectAdapter() *ObjectAdapterBuilder {
Expand All @@ -57,7 +61,7 @@ func (b *ObjectAdapterBuilder) SetHandler(value ObjectHandler) *ObjectAdapterBui
// SetIDVariable sets the name of the path variable that contains the identifier of the object. This is
// optional. If not specified then no identifier will be passed to the handler.
func (b *ObjectAdapterBuilder) SetIDVariable(value string) *ObjectAdapterBuilder {
b.id = value
b.idVariable = value
return b
}

Expand All @@ -73,6 +77,30 @@ func (b *ObjectAdapterBuilder) Build() (result *ObjectAdapter, err error) {
return
}

// Create the projector parser and evaluator:
projectorParser, err := search.NewProjectorParser().
SetLogger(b.logger).
Build()
if err != nil {
err = fmt.Errorf("failed to create field selector parser: %w", err)
return
}
pathEvaluator, err := search.NewPathEvaluator().
SetLogger(b.logger).
Build()
if err != nil {
err = fmt.Errorf("failed to create projector path evaluator: %w", err)
return
}
projectorEvaluator, err := search.NewProjectorEvaluator().
SetLogger(b.logger).
SetPathEvaluator(pathEvaluator.Evaluate).
Build()
if err != nil {
err = fmt.Errorf("failed to create projector evaluator: %w", err)
return
}

// Prepare the JSON iterator API:
jsonConfig := jsoniter.Config{
IndentionStep: 2,
Expand All @@ -81,10 +109,12 @@ func (b *ObjectAdapterBuilder) Build() (result *ObjectAdapter, err error) {

// Create and populate the object:
result = &ObjectAdapter{
logger: b.logger,
handler: b.handler,
id: b.id,
jsonAPI: jsonAPI,
logger: b.logger,
handler: b.handler,
idVariable: b.idVariable,
projectorParser: projectorParser,
projectorEvaluator: projectorEvaluator,
jsonAPI: jsonAPI,
}
return
}
Expand All @@ -100,16 +130,44 @@ func (a *ObjectAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the context:
ctx := r.Context()

// Get the identifier:
id := mux.Vars(r)[a.id]
// Get the query parameters:
query := r.URL.Query()

// Create the request:
request := &ObjectRequest{
ID: id,
ID: mux.Vars(r)[a.idVariable],
}

// Check if there is a projector, and parse it:
values, ok := query["fields"]
if ok {
for _, value := range values {
projector, err := a.projectorParser.Parse(value)
if err != nil {
a.logger.Error(
"Failed to parse field selector",
slog.String("selector", value),
slog.String("error", err.Error()),
)
SendError(
w,
http.StatusBadRequest,
"Failed to parse field selector '%s': %v",
value, err,
)
return
}
a.logger.Debug(
"Parsed field selector",
slog.String("source", value),
slog.Any("parsed", projector),
)
request.Projector = append(request.Projector, projector...)
}
}

// Call the handler:
response, err := a.handler.Get(r.Context(), request)
response, err := a.handler.Get(ctx, request)
if err != nil {
a.logger.Error(
"Failed to get items",
Expand All @@ -123,8 +181,26 @@ func (a *ObjectAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

// If there is a projector apply it:
object := response.Object
if request.Projector != nil {
object, err = a.projectorEvaluator.Evaluate(ctx, request.Projector, response.Object)
if err != nil {
a.logger.Error(
"Failed to evaluate projector",
slog.String("error", err.Error()),
)
SendError(
w,
http.StatusInternalServerError,
"Internal error",
)
return
}
}

// Send the result:
a.sendObject(ctx, w, response.Object)
a.sendObject(ctx, w, object)
}

func (a *ObjectAdapter) sendObject(ctx context.Context, w http.ResponseWriter,
Expand Down
Loading

0 comments on commit 9784fde

Please sign in to comment.