diff --git a/api/handlers/service_plan.go b/api/handlers/service_plan.go index bf6e82e69..73f367fe0 100644 --- a/api/handlers/service_plan.go +++ b/api/handlers/service_plan.go @@ -3,6 +3,7 @@ package handlers import ( "context" + "fmt" "net/http" "net/url" "slices" @@ -37,6 +38,7 @@ type ServicePlan struct { requestValidator RequestValidator servicePlanRepo CFServicePlanRepository serviceOfferingRepo CFServiceOfferingRepository + serviceBrokerRepo CFServiceBrokerRepository } func NewServicePlan( @@ -44,12 +46,14 @@ func NewServicePlan( requestValidator RequestValidator, servicePlanRepo CFServicePlanRepository, serviceOfferingRepo CFServiceOfferingRepository, + serviceBrokerRepo CFServiceBrokerRepository, ) *ServicePlan { return &ServicePlan{ serverURL: serverURL, requestValidator: requestValidator, servicePlanRepo: servicePlanRepo, serviceOfferingRepo: serviceOfferingRepo, + serviceBrokerRepo: serviceBrokerRepo, } } @@ -62,47 +66,110 @@ func (h *ServicePlan) list(r *http.Request) (*routing.Response, error) { return nil, apierrors.LogAndReturn(logger, err, "failed to decode json payload") } - servicePlanList, err := h.servicePlanRepo.ListPlans(r.Context(), authInfo, payload.ToMessage()) + servicePlans, err := h.servicePlanRepo.ListPlans(r.Context(), authInfo, payload.ToMessage()) if err != nil { return nil, apierrors.LogAndReturn(logger, err, "failed to list service plans") } + includedResources, err := h.getIncludedResources(r.Context(), authInfo, payload, servicePlans) + if err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to build included resources") + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForServicePlan, servicePlans, h.serverURL, *r.URL, includedResources...)), nil +} + +func (h *ServicePlan) getIncludedResources( + ctx context.Context, + authInfo authorization.Info, + payload payloads.ServicePlanList, + servicePlans []repositories.ServicePlanRecord, +) ([]model.IncludedResource, error) { + if len(payload.IncludeResources) == 0 && len(payload.IncludeBrokerFields) == 0 { + return nil, nil + } + includedResources := []model.IncludedResource{} + offerings, err := h.listOfferings(ctx, authInfo, servicePlans) + if err != nil { + return nil, fmt.Errorf("failed to list offerings for plans: %w", err) + } + if slices.Contains(payload.IncludeResources, "service_offering") { - includedOfferings, err := h.getIncludedServiceOfferings(r.Context(), authInfo, servicePlanList, h.serverURL) + includedResources = append(includedResources, iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) model.IncludedResource { + return model.IncludedResource{ + Type: "service_offerings", + Resource: presenter.ForServiceOffering(o, h.serverURL), + } + }).Collect()...) + } + + if len(payload.IncludeBrokerFields) != 0 { + brokers, err := h.listBrokers(ctx, authInfo, offerings) + if err != nil { + return nil, fmt.Errorf("failed to list brokers for offerings of plans: %w", err) + } + + includedBrokerFields, err := h.getIncludedBrokerFields(brokers, payload.IncludeBrokerFields) if err != nil { - return nil, apierrors.LogAndReturn(logger, err, "failed to get included service offerings") + return nil, fmt.Errorf("failed to get included broker fields: %w", err) } - includedResources = append(includedResources, includedOfferings...) + includedResources = append(includedResources, includedBrokerFields...) } - return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForServicePlan, servicePlanList, h.serverURL, *r.URL, includedResources...)), nil + return includedResources, nil } -func (h *ServicePlan) getIncludedServiceOfferings( +func (h *ServicePlan) listOfferings( ctx context.Context, authInfo authorization.Info, servicePlans []repositories.ServicePlanRecord, - baseUrl url.URL, -) ([]model.IncludedResource, error) { +) ([]repositories.ServiceOfferingRecord, error) { offeringGUIDs := iter.Map(iter.Lift(servicePlans), func(o repositories.ServicePlanRecord) string { return o.ServiceOfferingGUID }).Collect() - offerings, err := h.serviceOfferingRepo.ListOfferings(ctx, authInfo, repositories.ListServiceOfferingMessage{ + return h.serviceOfferingRepo.ListOfferings(ctx, authInfo, repositories.ListServiceOfferingMessage{ GUIDs: tools.Uniq(offeringGUIDs), }) - if err != nil { - return nil, err - } +} + +func (h *ServicePlan) listBrokers( + ctx context.Context, + authInfo authorization.Info, + offerings []repositories.ServiceOfferingRecord, +) ([]repositories.ServiceBrokerRecord, error) { + brokerGUIDs := iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) string { + return o.ServiceBrokerGUID + }).Collect() + + return h.serviceBrokerRepo.ListServiceBrokers(ctx, authInfo, repositories.ListServiceBrokerMessage{ + GUIDs: brokerGUIDs, + }) +} - return iter.Map(iter.Lift(offerings), func(o repositories.ServiceOfferingRecord) model.IncludedResource { +func (h *ServicePlan) getIncludedBrokerFields( + brokers []repositories.ServiceBrokerRecord, + brokerFields []string, +) ([]model.IncludedResource, error) { + brokerIncludes := iter.Map(iter.Lift(brokers), func(b repositories.ServiceBrokerRecord) model.IncludedResource { return model.IncludedResource{ - Type: "service_offerings", - Resource: presenter.ForServiceOffering(o, baseUrl), + Type: "service_brokers", + Resource: presenter.ForServiceBroker(b, h.serverURL), } - }).Collect(), nil + }).Collect() + + brokerIncludesFields := []model.IncludedResource{} + for _, brokerInclude := range brokerIncludes { + fields, err := brokerInclude.SelectJSONFields(brokerFields...) + if err != nil { + return nil, err + } + brokerIncludesFields = append(brokerIncludesFields, fields) + } + + return brokerIncludesFields, nil } func (h *ServicePlan) getPlanVisibility(r *http.Request) (*routing.Response, error) { diff --git a/api/handlers/service_plan_test.go b/api/handlers/service_plan_test.go index 51e317662..745bc08d1 100644 --- a/api/handlers/service_plan_test.go +++ b/api/handlers/service_plan_test.go @@ -22,6 +22,7 @@ var _ = Describe("ServicePlan", func() { var ( servicePlanRepo *fake.CFServicePlanRepository serviceOfferingRepo *fake.CFServiceOfferingRepository + serviceBrokerRepo *fake.CFServiceBrokerRepository requestValidator *fake.RequestValidator ) @@ -29,12 +30,14 @@ var _ = Describe("ServicePlan", func() { requestValidator = new(fake.RequestValidator) servicePlanRepo = new(fake.CFServicePlanRepository) serviceOfferingRepo = new(fake.CFServiceOfferingRepository) + serviceBrokerRepo = new(fake.CFServiceBrokerRepository) apiHandler := NewServicePlan( *serverURL, requestValidator, servicePlanRepo, serviceOfferingRepo, + serviceBrokerRepo, ) routerBuilder.LoadRoutes(apiHandler) }) @@ -121,7 +124,7 @@ var _ = Describe("ServicePlan", func() { }) }) - It("includes broker fields in the response", func() { + It("includes service offering in the response", func() { Expect(rr).Should(HaveHTTPStatus(http.StatusOK)) Expect(rr).To(HaveHTTPBody(SatisfyAll( MatchJSONPath("$.included.service_offerings[0].guid", "service-offering-guid"), @@ -130,6 +133,59 @@ var _ = Describe("ServicePlan", func() { }) }) + Describe("fields service_offering.service_broker", func() { + BeforeEach(func() { + serviceBrokerRepo.ListServiceBrokersReturns([]repositories.ServiceBrokerRecord{{ + ServiceBroker: services.ServiceBroker{ + Name: "service-broker-name", + }, + CFResource: model.CFResource{ + GUID: "service-broker-guid", + }, + }}, nil) + + serviceOfferingRepo.ListOfferingsReturns([]repositories.ServiceOfferingRecord{{ + ServiceOffering: services.ServiceOffering{ + Name: "service-offering-name", + }, + CFResource: model.CFResource{ + GUID: "service-offering-guid", + }, + ServiceBrokerGUID: "service-broker-guid", + }}, nil) + + requestValidator.DecodeAndValidateURLValuesStub = decodeAndValidateURLValuesStub(&payloads.ServicePlanList{ + IncludeBrokerFields: []string{"guid", "name"}, + }) + }) + + It("lists the brokers", func() { + Expect(serviceBrokerRepo.ListServiceBrokersCallCount()).To(Equal(1)) + _, _, actualListMessage := serviceBrokerRepo.ListServiceBrokersArgsForCall(0) + Expect(actualListMessage).To(Equal(repositories.ListServiceBrokerMessage{ + GUIDs: []string{"service-broker-guid"}, + })) + }) + + When("listing brokers fails", func() { + BeforeEach(func() { + serviceBrokerRepo.ListServiceBrokersReturns([]repositories.ServiceBrokerRecord{}, errors.New("list-broker-err")) + }) + + It("returns an error", func() { + expectUnknownError() + }) + }) + + It("includes broker fields in the response", func() { + Expect(rr).Should(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.included.service_brokers[0].guid", "service-broker-guid"), + MatchJSONPath("$.included.service_brokers[0].name", "service-broker-name"), + ))) + }) + }) + When("the request is invalid", func() { BeforeEach(func() { requestValidator.DecodeAndValidateURLValuesReturns(errors.New("invalid-request")) diff --git a/api/main.go b/api/main.go index 0aa8ffb0d..ef1d054a2 100644 --- a/api/main.go +++ b/api/main.go @@ -432,6 +432,7 @@ func main() { requestValidator, servicePlanRepo, serviceOfferingRepo, + serviceBrokerRepo, ), } for _, handler := range apiHandlers { diff --git a/api/payloads/service_plan.go b/api/payloads/service_plan.go index d97ff00b3..1fca834de 100644 --- a/api/payloads/service_plan.go +++ b/api/payloads/service_plan.go @@ -21,11 +21,13 @@ type ServicePlanList struct { Names string Available *bool IncludeResources []string + IncludeBrokerFields []string } func (l ServicePlanList) Validate() error { return jellidation.ValidateStruct(&l, jellidation.Field(&l.IncludeResources, jellidation.Each(validation.OneOf("service_offering"))), + jellidation.Field(&l.IncludeBrokerFields, jellidation.Each(validation.OneOf("guid", "name"))), ) } @@ -38,11 +40,11 @@ func (l *ServicePlanList) ToMessage() repositories.ListServicePlanMessage { } func (l *ServicePlanList) SupportedKeys() []string { - return []string{"service_offering_guids", "names", "available", "page", "per_page", "include"} + return []string{"service_offering_guids", "names", "available", "fields[service_offering.service_broker]", "page", "per_page", "include"} } func (l *ServicePlanList) IgnoredKeys() []*regexp.Regexp { - return []*regexp.Regexp{regexp.MustCompile(`fields\[.+\]`)} + return nil } func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error { @@ -55,6 +57,7 @@ func (l *ServicePlanList) DecodeFromURLValues(values url.Values) error { } l.Available = available l.IncludeResources = parse.ArrayParam(values.Get("include")) + l.IncludeBrokerFields = parse.ArrayParam(values.Get("fields[service_offering.service_broker]")) return nil } diff --git a/api/payloads/service_plan_test.go b/api/payloads/service_plan_test.go index 9a973b6de..98cc15743 100644 --- a/api/payloads/service_plan_test.go +++ b/api/payloads/service_plan_test.go @@ -25,6 +25,7 @@ var _ = Describe("ServicePlan", func() { Entry("available", "available=true", payloads.ServicePlanList{Available: tools.PtrTo(true)}), Entry("not available", "available=false", payloads.ServicePlanList{Available: tools.PtrTo(false)}), Entry("include", "include=service_offering", payloads.ServicePlanList{IncludeResources: []string{"service_offering"}}), + Entry("service broker fields", "fields[service_offering.service_broker]=guid,name", payloads.ServicePlanList{IncludeBrokerFields: []string{"guid", "name"}}), ) DescribeTable("invalid query", @@ -34,6 +35,7 @@ var _ = Describe("ServicePlan", func() { }, Entry("invalid available", "available=invalid", MatchError(ContainSubstring("failed to parse"))), Entry("invalid include", "include=foo", MatchError(ContainSubstring("value must be one of: service_offering"))), + Entry("invalid service broker fields", "fields[service_offering.service_broker]=foo", MatchError(ContainSubstring("value must be one of"))), ) Describe("ToMessage", func() {