چگونه یک فید پیشنهادات بسازیم

در آموزش زیر نحوه ساخت یک فید در JSON با استفاده از تعریف Google Protocol Buffers (protobuf) را نشان خواهیم داد. ما از کامپایلر protobuf برای تولید کد منبع بر اساس طرحواره protobuf استفاده خواهیم کرد. ساخت یک فید با استفاده از کد منبع تولید شده، رابط کاربری آسانی را فراهم می‌کند و از ایجاد موجودیت‌های فید با نام‌های فیلد یا انواع فیلد نادرست جلوگیری می‌کند.

راه‌اندازی پروژه

  • یک دایرکتوری پروژه جدید ایجاد کنید.
  • محتویات offer.proto money.proto dayofweek.proto timeofday.proto date.proto را از تعاریف proto به عنوان فایل‌های جدید در دایرکتوری ریشه پروژه خود کپی کنید.
  • کامپایلر protoc را نصب کنید

    برای تولید کد منبع از فایل‌های .proto، به کامپایلر protoc نیاز دارید. آخرین نسخه باینری از پیش ساخته شده را از صفحه انتشار Protocol Buffers GitHub دانلود کنید.

    فایل زیپ را از حالت فشرده خارج کنید و مسیر bin را به متغیر محیطی PATH اضافه کنید، برای مثال:

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

تولید کد منبع

پایتون

  1. کتابخانه پایتون protobuf را نصب کنید
    pip install protobuf
        
  2. تولید کد منبع کلاینت.

    دایرکتوری خروجی پروتو را ایجاد کنید: generated

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

    متغیر محیطی PYTHONPATH را به‌روزرسانی کنید تا مسیر تولید شده را شامل شود، برای مثال:

    export PYTHONPATH="$PYTHONPATH:./generated"
        

    اگر از Bazel استفاده می‌کنید، می‌توانید از دستور py_proto_library به عنوان جایگزینی برای اجرای protoc استفاده کنید.

مثال استفاده

نمونه کامل پروژه را می‌توانید اینجا پیدا کنید.

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()
    

این مثال کد، نحوه ایجاد یک فید پیشنهاد غذاخوری (DIING) را نشان می‌دهد. سپس نحوه سریالایز کردن فید به JSON را نشان می‌دهد.

API پایتون با تنظیم ویژگی‌ها، امکان مقداردهی اولیه‌ی تنبل (lazy initialization) اشیاء تودرتو را فراهم می‌کند.

جاوا

  1. وابستگی‌های protobuf-java و protobuf-java-util را با استفاده از maven یا gradle همانطور که در اینجا توضیح داده شده است، به پروژه خود اضافه کنید.
  2. تولید کد منبع کلاینت.
    protoc --java_out=src/main/java offer.proto money.proto dayofweek.proto timeofday.proto date.proto
        

    شما می‌توانید از protobuf-maven-plugin برای تولید کد منبع در زمان کامپایل هنگام استفاده از maven استفاده کنید.

    اگر از Bazel استفاده می‌کنید، می‌توانید از دستور java_proto_library به عنوان جایگزینی برای اجرای protoc استفاده کنید.

مثال استفاده

نمونه کامل پروژه را می‌توانید اینجا پیدا کنید.

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);
    }
}

    

این مثال کد، نحوه ایجاد یک فید پیشنهاد غذاخوری (DIING) را نشان می‌دهد. سپس نحوه سریالایز کردن فید به JSON را نشان می‌دهد.

برو

  1. افزونه protoc را برای go نصب کنید
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
        

    متغیر محیطی PATH را به‌روزرسانی کنید تا افزونه‌ی protoc-gen-go را شامل شود، برای مثال:

    export PATH="$PATH:$(go env GOPATH)/bin"
        
  2. برنامه را مقداردهی اولیه کنید و کد منبع کلاینت را تولید کنید.
    go mod init feed/app
    mkdir generated
    protoc --go_out=./generated/ offer.proto money.proto dayofweek.proto timeofday.proto date.proto
        

    اگر از Bazel استفاده می‌کنید، می‌توانید از دستور go_proto_library به عنوان جایگزینی برای اجرای protoc استفاده کنید.

مثال استفاده

نمونه کامل پروژه را می‌توانید اینجا پیدا کنید.

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))
}
    

این مثال کد، نحوه ایجاد یک فید پیشنهاد غذاخوری (DIING) را نشان می‌دهد. سپس نحوه سریالایز کردن فید به JSON را نشان می‌دهد.

تایپ اسکریپت

  1. افزونه TypeScript protoc را نصب کنید.
    npm init
    npm i -D typescript
    npm i ts-proto
          
    توجه داشته باشید که ts-proto یک پروژه رسمی پشتیبانی شده توسط گوگل نیست.
  2. دایرکتوری خروجی را ایجاد کنید و کد منبع کلاینت را تولید کنید.

    دایرکتوری خروجی پروتو را ایجاد کنید: 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
          

    اگر از Bazel استفاده می‌کنید، می‌توانید از قانون js_proto_library به عنوان جایگزینی برای اجرای protoc استفاده کنید.

مثال استفاده

نمونه کامل پروژه را می‌توانید اینجا پیدا کنید.

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();

  

این مثال کد، نحوه ایجاد یک فید پیشنهاد غذاخوری (DIING) را نشان می‌دهد. سپس نحوه سریالایز کردن فید به JSON را نشان می‌دهد.

نکته: افزونه‌های کامپایلر پروتوک شخص ثالثی برای تولید JSON Schema از proto schema وجود دارد. اگر ترجیح می‌دهید با فایل‌های JSON Schema کار کنید، می‌توانید یکی از این نمونه‌ها را اینجا بررسی کنید.