package mrktplaats import ( "context" "encoding/json" "fmt" "net/url" "strings" ) // SearchService handles listing search and keyword suggestions. type SearchService struct { t *transport } // DefaultSupportedTypes is the standard set of item types the client declares support for. var DefaultSupportedTypes = []string{ "defaultListing", "icasTopBlockListing", "icasSiblingListing", "sellerHeaderInformation", "srpHeaderNativeAd", "srpListNativeAd", "displayBanner", "similarItems", "srpSaveSearchFooter", "lrpDisclaimer", "shippingPromotion", "dac7banner", "knnInfoBox", } // SearchRequest defines the parameters for a listing search. type SearchRequest struct { CategoryID *int `json:"categoryId,omitempty"` Query string `json:"q"` Page int `json:"page"` Size int `json:"size"` SortBy string `json:"sortBy"` Languages []string `json:"languages,omitempty"` Latitude float64 `json:"latitude,omitempty"` Longitude float64 `json:"longitude,omitempty"` AllowCorrection bool `json:"allowCorrection"` SearchOnTitleAndDescription bool `json:"searchOnTitleAndDescription"` ShowListings bool `json:"showListings"` ShowSimilarItems bool `json:"showSimilarItems"` SupportsReservedFlag bool `json:"supportsReservedFlag"` LrpDisclaimerDismissTime int `json:"lrpDisclaimerDismissTime"` AvailableTilesInLastRow int `json:"availableTilesInLastRow"` SellerID string `json:"sellerId,omitempty"` AttributesById []AttributeFilter `json:"attributesById,omitempty"` } // AttributeFilter filters search results by a specific attribute key-value pair. type AttributeFilter struct { AttributeKeyID string `json:"attributeKeyId"` AttributeValueID string `json:"attributeValueId"` } type searchRequestEnvelope struct { SearchRequest SearchRequest `json:"searchRequest"` SupportedTypes []string `json:"supportedTypes"` } // SearchResponse contains the search results. type SearchResponse struct { Items []SearchItem `json:"items"` SearchHistograms *SearchHistograms `json:"searchHistograms,omitempty"` CorrelationID string `json:"correlationId"` HasErrors bool `json:"hasErrors"` CategoryID int `json:"categoryId"` BigViewAllowed bool `json:"bigViewAllowed"` } // SearchHistograms contains facet/filter information from a search. type SearchHistograms struct { NumFound int `json:"numFound"` Attributes json.RawMessage `json:"attributes,omitempty"` Categories json.RawMessage `json:"categories,omitempty"` SortableFields json.RawMessage `json:"sortableFields,omitempty"` } // SearchItem is a single item in search results. The ItemType field // determines which field is populated. type SearchItem struct { ItemType string `json:"itemType"` DefaultListing *DefaultListing `json:"defaultListing,omitempty"` IcasTopBlockListing *DefaultListing `json:"icasTopBlockListing,omitempty"` IcasSiblingListing *DefaultListing `json:"icasSiblingListing,omitempty"` } // Listing returns the first non-nil listing from the item, regardless of type. // Returns nil for non-listing item types (banners, disclaimers, etc.). func (i *SearchItem) Listing() *DefaultListing { switch { case i.DefaultListing != nil: return i.DefaultListing case i.IcasTopBlockListing != nil: return i.IcasTopBlockListing case i.IcasSiblingListing != nil: return i.IcasSiblingListing default: return nil } } // DefaultListing represents a standard listing in search results. type DefaultListing struct { AdCore AdCore `json:"adCore"` DistanceInMeter int `json:"distanceInMeter,omitempty"` LocationDescription string `json:"locationDescription,omitempty"` SellerInformation SellerInformation `json:"sellerInformation"` URL string `json:"url,omitempty"` ExtraImages json.RawMessage `json:"extraImages,omitempty"` Thumbnails json.RawMessage `json:"thumbnails,omitempty"` TotalPictures int `json:"totalPictures,omitempty"` AdType string `json:"adType,omitempty"` SortDateTime string `json:"sortDateTime,omitempty"` Flags json.RawMessage `json:"flags,omitempty"` SavedAdForUser bool `json:"savedAdForUser,omitempty"` Shippable bool `json:"shippable,omitempty"` Reserved bool `json:"reserved,omitempty"` PageLocation string `json:"pageLocation,omitempty"` } // KeywordSuggestionsResponse is the response from keyword autocomplete. type KeywordSuggestionsResponse struct { Keyword string `json:"keyword"` Category int `json:"category"` Suggestions []string `json:"suggestions"` } // KeywordSuggestions returns autocomplete suggestions for a search prefix. func (s *SearchService) KeywordSuggestions(ctx context.Context, prefix string, category int) (*KeywordSuggestionsResponse, error) { params := url.Values{ "prefix": {prefix}, "category": {fmt.Sprintf("%d", category)}, } var resp KeywordSuggestionsResponse if err := s.t.get(ctx, "/app/lrp/v1/keyword-suggestions", params, &resp); err != nil { return nil, err } return &resp, nil } // Fetch performs a listing search with the given request. // If supportedTypes is nil, DefaultSupportedTypes is used. func (s *SearchService) Fetch(ctx context.Context, req *SearchRequest, supportedTypes []string) (*SearchResponse, error) { if supportedTypes == nil { supportedTypes = DefaultSupportedTypes } envelope := searchRequestEnvelope{ SearchRequest: *req, SupportedTypes: supportedTypes, } var resp SearchResponse if err := s.t.postJSON(ctx, "/app/lrp/v1/items/fetch", nil, nil, envelope, &resp); err != nil { return nil, err } return &resp, nil } // DisplayTargetingRequest defines parameters for search ad targeting. // Positions is a slice of position strings that will be serialized as a // comma-separated string as required by the API. // Postcode and SellerID are sent as JSON null when empty. type DisplayTargetingRequest struct { Attr json.RawMessage Positions []string Query string SearchCategoryID int Postcode string SellerID string } // MarshalJSON serializes the request in the format the marktplaats API expects: // positions as a comma-separated string, attr as {} when nil, and // postcode/sellerId as null when empty. func (r DisplayTargetingRequest) MarshalJSON() ([]byte, error) { attr := r.Attr if len(attr) == 0 { attr = json.RawMessage(`{}`) } type wire struct { Attr json.RawMessage `json:"attr"` Positions string `json:"positions"` Query string `json:"q"` SearchCategoryID int `json:"searchCategoryId"` Postcode *string `json:"postcode"` SellerID *string `json:"sellerId"` } w := wire{ Attr: attr, Positions: strings.Join(r.Positions, ", "), Query: r.Query, SearchCategoryID: r.SearchCategoryID, } if r.Postcode != "" { w.Postcode = &r.Postcode } if r.SellerID != "" { w.SellerID = &r.SellerID } return json.Marshal(w) } // DisplayTargetingResult is a single ad slot targeting result. type DisplayTargetingResult struct { Position string `json:"position"` AdUnitID string `json:"adUnitId"` AdditionalParameters map[string][]string `json:"additionalParameters"` } // DisplayTargeting retrieves ad targeting configuration for search results. func (s *SearchService) DisplayTargeting(ctx context.Context, req *DisplayTargetingRequest) ([]DisplayTargetingResult, error) { var resp []DisplayTargetingResult if err := s.t.postJSON(ctx, "/app/lrp/v1/display-targeting", nil, nil, req, &resp); err != nil { return nil, err } return resp, nil }