Cómo crear un feed de ofertas

En el siguiente instructivo, mostraremos cómo compilar un feed en JSON con la definición de búferes de protocolo de Google (protobuf). Usaremos el compilador de protobuf para generar código fuente basado en el esquema de protobuf. Compilar un feed con el código fuente generado proporciona una interfaz sencilla y evita que las entidades del feed se creen con nombres o tipos de campos incorrectos.

Configura el proyecto

  • Crea un directorio de proyecto nuevo.
  • Copia el contenido de offer.proto money.proto dayofweek.proto timeofday.proto date.proto de las definiciones de proto como archivos nuevos en la raíz del directorio de tu proyecto.
  • Instala el compilador protoc

    Para generar código fuente a partir de los archivos .proto, necesitarás el compilador protoc. Descarga el archivo binario prediseñado más reciente desde la página de versiones de Protocol Buffers en GitHub.

    Extrae el archivo zip y agrega la ruta de acceso binaria a la variable de entorno PATH, por ejemplo:

    unzip protoc-22.0-linux-x86_64.zip -d /usr/local/protoc
    export PATH="$PATH:/usr/local/protoc/bin"
      

Genera código fuente

Python

  1. Instala la biblioteca de Python protobuf
    pip install protobuf
        
  2. Genera el código fuente del cliente.

    Crea el directorio de salida de proto: generated

    protoc --python_out=./generated offer.proto money.proto dayofweek.proto timeofday.proto date.proto
        

    Actualiza la variable de entorno PYTHONPATH para incluir la ruta de acceso generada, por ejemplo:

    export PYTHONPATH="$PYTHONPATH:./generated"
        

    Si usas Bazel, prueba la regla py_proto_library como alternativa a la ejecución de protoc.

Ejemplo de uso

Puedes encontrar el proyecto de ejemplo completo aquí.

import json
from google.protobuf.json_format import MessageToDict
from google.protobuf.timestamp_pb2 import Timestamp


# Replace these imports with your actual generated proto package paths
from generated import offer_pb2
from generated import money_pb2
from generated import dayofweek_pb2
from generated import timeofday_pb2

_MAX_BYTES_DATA_FILE = 200 * 1024 * 1024

def generate_offer_feed():

    # Create OfferFeed
    feed = offer_pb2.OfferFeed()

    # Build the offers
    offer = offer_pb2.Offer(
        offer_id="offer-1",
        entity_ids=["dining-1"],
        offer_source=offer_pb2.OFFER_SOURCE_AGGREGATOR,
        action_type=offer_pb2.ACTION_TYPE_DINING,
        offer_modes=[
            offer_pb2.OFFER_MODE_WALK_IN,
            offer_pb2.OFFER_MODE_FREE_RESERVATION
        ],
        offer_category=offer_pb2.OFFER_CATEGORY_BASE_OFFER,
        offer_details=offer_pb2.OfferDetails(
            offer_display_text="₹100 off on your order",
            # Note: If this is a 'oneof', you set the field name directly
            discount_value=money_pb2.Money(
                currency_code="INR",
                units=100
            )
        ),
        offer_restrictions=offer_pb2.OfferRestrictions(
            combinable_with_other_offers=True,
            combinable_offer_categories=[
                offer_pb2.OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER,
                offer_pb2.OFFER_CATEGORY_ADD_ON_COUPON_OFFER
            ]
        ),
        terms=offer_pb2.Terms(
            restricted_to_certain_users=False,
            terms_and_conditions="Valid on all menu items."
        ),
        validity_periods=[
            offer_pb2.ValidityPeriod(
                valid_period=offer_pb2.ValidityRange(
                    valid_from_time=Timestamp(seconds=1687062000),
                    valid_through_time=Timestamp(seconds=1956556800)
                ),
                time_of_day=[
                    # Monday - Thursday Window
                    offer_pb2.TimeOfDayWindow(
                        time_windows=offer_pb2.TimeOfDayRange(
                            open_time=timeofday_pb2.TimeOfDay(hours=13),
                            close_time=timeofday_pb2.TimeOfDay(hours=23)
                        ),
                        day_of_week=[
                            dayofweek_pb2.DayOfWeek.MONDAY,
                            dayofweek_pb2.DayOfWeek.TUESDAY,
                            dayofweek_pb2.DayOfWeek.WEDNESDAY,
                            dayofweek_pb2.DayOfWeek.THURSDAY
                        ]
                    ),
                    # Friday - Sunday Window
                    offer_pb2.TimeOfDayWindow(
                        time_windows=offer_pb2.TimeOfDayRange(
                            open_time=timeofday_pb2.TimeOfDay(hours=13),
                            close_time=timeofday_pb2.TimeOfDay(hours=23, minutes=59, seconds=59)
                        ),
                        day_of_week=[
                            dayofweek_pb2.DayOfWeek.FRIDAY,
                            dayofweek_pb2.DayOfWeek.SATURDAY,
                            dayofweek_pb2.DayOfWeek.SUNDAY
                        ]
                    )
                ]
            )
        ],
        offer_url="https://www.example-restaurant.com/offer/base_offer_1",
        image_url="https://www.example-restaurant.com/images/offer_base.jpg"
    )

    # Example testing for menu feed size
    # Protocol buffer message must be less than 2 GiB
    # https://protobuf.dev/programming-guides/proto-limits/
    # It is recommended to not exceed 200 MB, as there is an Actions
    # Center limit of 200 MB per file after compression.
    if feed.ByteSize() + offer.ByteSize()  < _MAX_BYTES_DATA_FILE:
	    feed.data.append(offer)
    # else write current feed to file and start a new feed

    # Serialize to JSON
    # preserving_proto_field_names=True ensures camelCase becomes snake_case if defined that way in .proto
    json_output = json.dumps(
        MessageToDict(feed, preserving_proto_field_name=True)
    )

    print(json_output)

if __name__ == "__main__":
    generate_offer_feed()
    

En el ejemplo de código, se muestra cómo crear un feed de ofertas de DINING. Luego, el ejemplo muestra cómo serializar el feed en JSON.

La API de Python permite la inicialización diferida de objetos anidados estableciendo las propiedades.

Java

  1. Agrega las dependencias protobuf-java y protobuf-java-util a tu proyecto con Maven o Gradle, como se describe aquí.
  2. Genera el código fuente del cliente.
    protoc --java_out=src/main/java offer.proto money.proto dayofweek.proto timeofday.proto date.proto
        

    Puedes usar protobuf-maven-plugin para generar el código fuente durante el tiempo de compilación cuando usas Maven.

    Si usas Bazel, prueba la regla java_proto_library como alternativa para ejecutar protoc.

Ejemplo de uso

Puedes encontrar el proyecto de ejemplo completo aquí.

package com.example.offers;

import com.google.protobuf.util.JsonFormat;
import ext.maps.booking.feeds.offers.Offer;
import ext.maps.booking.feeds.offers.OfferFeed;
import ext.maps.booking.feeds.offers.OfferDetails;
import ext.maps.booking.feeds.offers.OfferRestrictions;
import ext.maps.booking.feeds.offers.Terms;
import ext.maps.booking.feeds.offers.ValidityPeriod;
import ext.maps.booking.feeds.offers.ValidityRange;
import ext.maps.booking.feeds.offers.OfferCategory;
import ext.maps.booking.feeds.offers.OfferMode;
import ext.maps.booking.feeds.offers.OfferSource;
import ext.maps.booking.feeds.offers.ActionType;
import ext.maps.booking.feeds.offers.TimeOfDayWindow;
import ext.maps.booking.feeds.offers.TimeOfDayRange;

import com.google.type.Money;
import com.google.protobuf.Timestamp;
import com.google.type.DayOfWeek;
import com.google.type.TimeOfDay;


public class OfferFeedGenerator {

    // 200 MB
    public static final int MAX_BYTES_DATA_FILE = 200 * 1024 * 1024;
  
    public static void main(String[] args) throws Exception {
        
        // Create OfferFeed
        OfferFeed.Builder feed = OfferFeed.newBuilder();

        // Build the offers
        Offer offer = Offer.newBuilder()
            .setOfferId("offer-1")
            .addEntityIds("dining-1")
            .setOfferSource(OfferSource.OFFER_SOURCE_AGGREGATOR)
            .setActionType(ActionType.ACTION_TYPE_DINING)
            .addOfferModes(OfferMode.OFFER_MODE_WALK_IN)
            .addOfferModes(OfferMode.OFFER_MODE_FREE_RESERVATION)
            .setOfferCategory(OfferCategory.OFFER_CATEGORY_BASE_OFFER)
            .setOfferDetails(OfferDetails.newBuilder()
                .setOfferDisplayText("₹100 off on your order")
                .setDiscountValue(Money.newBuilder()
                    .setCurrencyCode("INR")
                    .setUnits(100)))
            .setOfferRestrictions(OfferRestrictions.newBuilder()
                .setCombinableWithOtherOffers(true)
                .addCombinableOfferCategories(OfferCategory.OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER)
                .addCombinableOfferCategories(OfferCategory.OFFER_CATEGORY_ADD_ON_COUPON_OFFER))
            .setTerms(Terms.newBuilder()
                .setRestrictedToCertainUsers(false)
                .setTermsAndConditions("Valid on all menu items."))
            .addValidityPeriods(ValidityPeriod.newBuilder()
                .setValidPeriod(ext.maps.booking.feeds.offers.ValidityRange.newBuilder()
                    .setValidFromTime(Timestamp.newBuilder().setSeconds(1687062000))
                    .setValidThroughTime(Timestamp.newBuilder().setSeconds(1956556800)))
                // // Monday - Thursday Window
                .addTimeOfDay(ext.maps.booking.feeds.offers.TimeOfDayWindow.newBuilder()
                    .setTimeWindows(ext.maps.booking.feeds.offers.TimeOfDayRange.newBuilder()
                        .setOpenTime(com.google.type.TimeOfDay.newBuilder().setHours(13))
                        .setCloseTime(com.google.type.TimeOfDay.newBuilder().setHours(23)))
                    .addDayOfWeek(DayOfWeek.MONDAY)
                    .addDayOfWeek(DayOfWeek.TUESDAY)
                    .addDayOfWeek(DayOfWeek.WEDNESDAY)
                    .addDayOfWeek(DayOfWeek.THURSDAY))
                // Friday - Sunday Window
                .addTimeOfDay(ext.maps.booking.feeds.offers.TimeOfDayWindow.newBuilder()
                    .setTimeWindows(ext.maps.booking.feeds.offers.TimeOfDayRange.newBuilder()
                        .setOpenTime(com.google.type.TimeOfDay.newBuilder().setHours(13))
                        .setCloseTime(com.google.type.TimeOfDay.newBuilder().setHours(23).setMinutes(59).setSeconds(59)))
                    .addDayOfWeek(DayOfWeek.FRIDAY)
                    .addDayOfWeek(DayOfWeek.SATURDAY)
                    .addDayOfWeek(DayOfWeek.SUNDAY))
            )
            .setOfferUrl("https://www.example-restaurant.com/offer/base_offer_1")
            .setImageUrl("https://www.example-restaurant.com/images/offer_base.jpg")
            .build();


        // Example testing for offer feed size
        // Protocol buffer message must be less than 2 GiB
        // https://protobuf.dev/programming-guides/proto-limits/
        // It is recommended to not exceed 200 MB, as there is an Actions
        // Center limit of 200 MB per file after compression.
        int offerSize = offer.getSerializedSize();
        int currentFeedSize = feed.build().getSerializedSize();

        if (currentFeedSize + offerSize < MAX_BYTES_DATA_FILE) {
            feed.addData(offer);
        } else {
            // 1. Serialize and save the current 'feed' to a file
            // 2. Reset 'feed' to a new empty OfferFeed
            // 3. Add the 'offer' to the new feed
        }

        // Serialize to JSON
        String jsonOutput = JsonFormat.printer()
            .preservingProtoFieldNames()
            .print(feed);

        System.out.println(jsonOutput);
    }
}

    

En el ejemplo de código, se muestra cómo crear un feed de ofertas de DINING. Luego, el ejemplo muestra cómo serializar el feed en JSON.

Go

  1. Instala el complemento protoc para Go
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
        

    Actualiza la variable de entorno PATH para incluir el complemento protoc-gen-go, por ejemplo:

    export PATH="$PATH:$(go env GOPATH)/bin"
        
  2. Inicializa la app y genera el código fuente del cliente.
    go mod init feed/app
    mkdir generated
    protoc --go_out=./generated/ offer.proto money.proto dayofweek.proto timeofday.proto date.proto
        

    Si usas Bazel, prueba la regla go_proto_library como alternativa a la ejecución de protoc.

Ejemplo de uso

Puedes encontrar el proyecto de ejemplo completo aquí.

package main

import (
	pb "feed/app/generated/ext/maps/booking/offers/proto"
	"fmt"
	"log"

	money "google.golang.org/genproto/googleapis/type/money"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
	dayofweek "google.golang.org/genproto/googleapis/type/dayofweek"
	timeofday "google.golang.org/genproto/googleapis/type/timeofday"
	proto "google.golang.org/protobuf/proto"

	"google.golang.org/protobuf/encoding/protojson"
)

func main() {
	// 200 MB feed file size limit
	const MaxBytesDataFile = 200 * 1024 * 1024 

	// Create OfferFeed with offers
	feed := &pb.OfferFeed{}

	// Build the offers
	offer := &pb.Offer{
		OfferId:   "offer-1",
		EntityIds: []string{"dining-1"},
		OfferSource: pb.OfferSource_OFFER_SOURCE_AGGREGATOR,
		ActionType:  pb.ActionType_ACTION_TYPE_DINING,
		OfferModes: []pb.OfferMode{
			pb.OfferMode_OFFER_MODE_WALK_IN,
			pb.OfferMode_OFFER_MODE_FREE_RESERVATION,
		},
		OfferCategory: pb.OfferCategory_OFFER_CATEGORY_BASE_OFFER,
		OfferDetails: &pb.OfferDetails{
			OfferDisplayText: "₹100 off on your order",
			OfferSpecification: &pb.OfferDetails_DiscountValue{
        		DiscountValue: &money.Money{
            		CurrencyCode: "INR",
            		Units:        100,
				},
			},
		},
		OfferRestrictions: &pb.OfferRestrictions{
			CombinableWithOtherOffers: true,
			CombinableOfferCategories: []pb.OfferCategory{
				pb.OfferCategory_OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER,
				pb.OfferCategory_OFFER_CATEGORY_ADD_ON_COUPON_OFFER,
			},
		},
		Terms: &pb.Terms{
			RestrictedToCertainUsers: false,
			TermsAndConditions:       "Valid on all menu items.",
		},
		ValidityPeriods: []*pb.ValidityPeriod{
			{
				ValidPeriod: &pb.ValidityRange{
					ValidFromTime:    &timestamppb.Timestamp{Seconds: 1687062000},
					ValidThroughTime: &timestamppb.Timestamp{Seconds: 1956556800},
				},
				TimeOfDay: []*pb.TimeOfDayWindow{
					// Monday - Thursday Window
					{
						TimeWindows: &pb.TimeOfDayRange{
							OpenTime:  &timeofday.TimeOfDay{Hours: 13},
							CloseTime: &timeofday.TimeOfDay{Hours: 23},
						},
						DayOfWeek: []dayofweek.DayOfWeek{
							dayofweek.DayOfWeek_MONDAY,
							dayofweek.DayOfWeek_TUESDAY,
							dayofweek.DayOfWeek_WEDNESDAY,
							dayofweek.DayOfWeek_THURSDAY,
						},
					},
					// Friday - Sunday Window
					{
						TimeWindows: &pb.TimeOfDayRange{
							OpenTime: &timeofday.TimeOfDay{Hours: 13},
							CloseTime: &timeofday.TimeOfDay{
								Hours:   23,
								Minutes: 59,
								Seconds: 59,
							},
						},
						DayOfWeek: []dayofweek.DayOfWeek{
							dayofweek.DayOfWeek_FRIDAY,
							dayofweek.DayOfWeek_SATURDAY,
							dayofweek.DayOfWeek_SUNDAY,
						},
					},
				},
			},
		},
		OfferUrl: "https://www.example-restaurant.com/offer/base_offer_1",
		ImageUrl: "https://www.example-restaurant.com/images/offer_base.jpg",
	}

	// Example testing for feed size
	// Protocol buffer message must be less than 2 GiB
	// https://protobuf.dev/programming-guides/proto-limits/
	// It is recommended to not exceed 200 MB, as there is an Actions
	// Center limit of 200 MB per file after compression.
	offerSize := proto.Size(offer)
	currentFeedSize := proto.Size(feed)

	if currentFeedSize + offerSize < MaxBytesDataFile {
		feed.Data = append(feed.Data, offer)
	} else {
		// 1. Serialize and save the current 'feed' to a file
		// 2. Reset 'feed' to a new empty OfferFeed
		// 3. Add the 'offer' to the new feed
	}

	// Serialize to JSON
	marshaler := protojson.MarshalOptions{
		UseProtoNames: true,
	}

	jsonOutput, err := marshaler.Marshal(feed)
	if err != nil {
		log.Fatalf("Failed to marshal feed: %v", err)
	}

	fmt.Println(string(jsonOutput))
}
    

En el ejemplo de código, se muestra cómo crear un feed de ofertas de DINING. Luego, el ejemplo muestra cómo serializar el feed en JSON.

TypeScript

  1. Instala el complemento de protoc de TypeScript.
    npm init
    npm i -D typescript
    npm i ts-proto
          
    Ten en cuenta que ts-proto no es un proyecto de Google con asistencia oficial.
  2. Crea el directorio de salida y genera el código fuente del cliente.

    Crea el directorio de salida de proto: src/generated

    protoc --plugin="./node_modules/.bin/protoc-gen-ts_proto" --ts_proto_opt=useOptionals=all --ts_proto_opt=snakeToCamel=false --ts_proto_opt=onlyTypes=true --ts_proto_out="./src/generated" offer.proto money.proto dayofweek.proto timeofday.proto date.proto
          

    Si usas Bazel, prueba la regla js_proto_library como alternativa para ejecutar protoc.

Ejemplo de uso

Puedes encontrar el proyecto de ejemplo completo aquí.

import { 
  Offer, 
  OfferFeed, 
  OfferSource, 
  ActionType, 
  OfferMode, 
  OfferCategory 
} from "./generated/offer"; // Path to your generated types
import { Money } from "./generated/money";
import { DayOfWeek } from "./generated/dayofweek";
import { TimeOfDay } from "./generated/timeofday";
import { Timestamp } from "./generated/google/protobuf/timestamp";
import { Duration } from "./generated/google/protobuf/duration";

// 200 MB Limit
const MAX_BYTES_DATA_FILE = 200 * 1024 * 1024;

function generateOfferFeed() {
  // Build the Offer object
  const offer: Offer = {
    offer_id: "offer-1",
    entity_ids: ["dining-1"],
    offer_source: OfferSource.OFFER_SOURCE_AGGREGATOR,
    action_type: ActionType.ACTION_TYPE_DINING,
    offer_modes: [
      OfferMode.OFFER_MODE_WALK_IN,
      OfferMode.OFFER_MODE_FREE_RESERVATION,
    ],
    offer_category: OfferCategory.OFFER_CATEGORY_BASE_OFFER,
    offer_details: {
      offer_display_text: "₹100 off on your order",
      // Set 'oneof' field: discountValue
      discount_value: {
        currency_code: "INR",
        units: 100,
      },
    },
    offer_restrictions: {
      combinable_with_other_offers: true,
      combinable_offer_categories: [
        OfferCategory.OFFER_CATEGORY_ADD_ON_PAYMENT_OFFER,
        OfferCategory.OFFER_CATEGORY_ADD_ON_COUPON_OFFER,
      ],
    },
    terms: {
      restricted_to_certain_users: false,
      terms_and_conditions: "Valid on all menu items.",
    },
    validity_periods: [
      {
        valid_period: {
          valid_from_time: new Date(1687062000000),
          valid_through_time: new Date(1956556800000),
        },
        time_of_day: [
          // Monday - Thursday Window
          {
            time_windows: {
              open_time: { hours: 13, minutes: 0, seconds: 0, nanos: 0 },
              close_time: { hours: 23, minutes: 0, seconds: 0, nanos: 0 },
            },
            day_of_week: [
              DayOfWeek.MONDAY,
              DayOfWeek.TUESDAY,
              DayOfWeek.WEDNESDAY,
              DayOfWeek.THURSDAY,
            ],
          },
          // Friday - Sunday Window
          {
            time_windows: {
              open_time: { hours: 13, minutes: 0, seconds: 0, nanos: 0 },
              close_time: { hours: 23, minutes: 59, seconds: 59, nanos: 0 },
            },
            day_of_week: [
              DayOfWeek.FRIDAY,
              DayOfWeek.SATURDAY,
              DayOfWeek.SUNDAY,
            ],
          },
        ],
      },
    ],
    offer_url: "https://www.example-restaurant.com/offer/base_offer_1",
    image_url: "https://www.example-restaurant.com/images/offer_base.jpg",
  };

  // 2. Initialize the Feed
  const feed: OfferFeed = { data: [] };

  // 3. Size checking
  // encode().finish() returns a Uint8Array (the binary representation)
  const offerSize = new Blob([JSON.stringify(offer)]).size;
  const currentFeedSize = new Blob([JSON.stringify(feed)]).size;
  
  if (currentFeedSize + offerSize < MAX_BYTES_DATA_FILE) {
    if (feed.data == undefined) {
      feed.data = [offer]; 
    } else {
      feed.data.push(offer);
    }
  } else {
    // Logic for file rotation goes here
    console.log("Feed size limit reached. Rotate file.");
  }

  // 4. Serialize to JSON
  // In JS/TS, we often just use JSON.stringify for the plain object
  // if you used proto3 and want to ensure proper enum/timestamp formatting,
  // use the library's toJSON method.
  const jsonOutput = JSON.stringify(feed);
  console.log(jsonOutput);
}

generateOfferFeed();

  

En el ejemplo de código, se muestra cómo crear un feed de ofertas de DINING. Luego, el ejemplo muestra cómo serializar el feed en JSON.

Nota: Existen extensiones de compilador de protoc de terceros para generar esquemas JSON a partir del esquema de .proto. Si prefieres trabajar con archivos de esquema JSON, puedes revisar un ejemplo aquí.