Files
mrktplaats/search.go
T
Joren 25410749db Initial Marktplaats SDK scaffold
Clone the twdehands SDK into mrktplaats and retarget naming and defaults for app.marktplaats.nl while keeping the same request/response bodies and endpoint behavior.
2026-04-15 23:45:49 +02:00

223 lines
8.2 KiB
Go

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
}