diff --git a/Makefile b/Makefile index 5b4d6903..dc0022a5 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ PROTOS_DIR=$(EXAMPLE_DIR)/proto EXAMPLE_CMD=bin/protoc --plugin=bin/protoc-gen-doc \ -Ithirdparty -Itmp/googleapis -Iexamples/proto \ --doc_out=examples/doc +EXAMPLE_CMD_MULTI_PAGE=bin/protoc --plugin=bin/protoc-gen-doc \ + -Ithirdparty -Itmp/googleapis -Iexamples/proto \ + --doc_out=examples/doc-multi-page DOCKER_CMD=docker run --rm \ -v $(DOCS_DIR):/out:rw \ @@ -44,6 +47,7 @@ build/examples: bin/protoc build tmp/googleapis examples/proto/*.proto examples/ @$(EXAMPLE_CMD) --doc_opt=json,example.json:Ignore* examples/proto/*.proto @$(EXAMPLE_CMD) --doc_opt=markdown,example.md:Ignore* examples/proto/*.proto @$(EXAMPLE_CMD) --doc_opt=examples/templates/asciidoc.tmpl,example.txt:Ignore* examples/proto/*.proto + @$(EXAMPLE_CMD_MULTI_PAGE)/html --doc_opt=examples/templates/html-multi-page.tmpl,html,source_relative,separate_files:Ignore* examples/proto/*.proto ##@: Dev diff --git a/README.md b/README.md index 2bdc8587..68c386c2 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,25 @@ example](examples/gradle). The plugin is invoked by passing the `--doc_out`, and `--doc_opt` options to the `protoc` compiler. The option has the following format: - --doc_opt=|,[,default|source_relative] + --doc_opt=|,[,default|source_relative][,default|separate_files] The format may be one of the built-in ones ( `docbook`, `html`, `markdown` or `json`) or the name of a file containing a custom [Go template][gotemplate]. +### `source_relative` + If the `source_relative` flag is specified, the output file is written in the same relative directory as the input file. +### `separate_files` + +If the `separate_files` flag is specified, there will be one file outputted per input file and the second parameter, +``, will be used as the extension of the outputted files. + +For example, the following will result in the outputted files `foo.md` and `bar.md` since `md` is passed as the second parameter. +``` +--doc_opt=markdown,md,source_relative,separate_files foo.proto bar.proto +``` + ### Using the Docker Image (Recommended) The docker image has two volumes: `/out` and `/protos` which are the directory to write the documentation to and the diff --git a/cmd/protoc-gen-doc/flags.go b/cmd/protoc-gen-doc/flags.go index 40aa8976..9ae7cc75 100644 --- a/cmd/protoc-gen-doc/flags.go +++ b/cmd/protoc-gen-doc/flags.go @@ -24,6 +24,9 @@ protoc --doc_out=. --doc_opt=custom.tmpl,docs.txt protos/*.proto EXAMPLE: Generate docs relative to source protos protoc --doc_out=. --doc_opt=html,index.html,source_relative protos/*.proto +EXAMPLE: Generate docs relative to source protos, one output file per input file +protoc --doc_out=. --doc_opt=html,html,source_relative,separate_files protos/*.proto + See https://github.com/pseudomuto/protoc-gen-doc for more details. ` diff --git a/examples/doc-multi-page/html/Booking.html b/examples/doc-multi-page/html/Booking.html new file mode 100644 index 00000000..97730738 --- /dev/null +++ b/examples/doc-multi-page/html/Booking.html @@ -0,0 +1,472 @@ + + + + + Protocol Documentation + + + + + + + + + + +

Booking.proto

+

Booking related messages.

This file is really just an example. The data model is completely

fictional.

+ +

Table of Contents

+ +
+ +
+ + + + +

Services

+ + +

BookingService

+

Service for handling vehicle bookings.

+ + + + + + + + + + + + + + + + + + + + + +
Method NameRequest TypeResponse TypeDescription
BookVehicleBookingBookingStatus

Used to book a vehicle. Pass in a Booking and a BookingStatus will be returned.

BookingUpdatesBookingStatusIDBookingStatus stream

Used to subscribe to updates of the BookingStatus.

+ + + + +

Methods with HTTP bindings

+ + + + + + + + + + + + + + + + + + + + + + +
Method NameMethodPatternBody
BookVehiclePOST/api/bookings/vehicle/{vehicle_id}*
+ + + + +

Messages

+ + +

Booking

+

Represents the booking of a vehicle.

Vehicles are some cool shit. But drive carefully!

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
vehicle_idint32

ID of booked vehicle.

customer_idint32

Customer that booked the vehicle.

statusBookingStatus

Status of the booking.

confirmation_sentbool

Has booking confirmation been sent?

payment_receivedbool

Has payment been received?

color_preferencestring

Deprecated. Color preference of the customer.

+ + + + +

Fields with deprecated option

+ + + + + + + + + + + + + + + +
NameOption
color_preference

true

+ + + + + +

BookingStatus

+

Represents the status of a vehicle booking.

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idint32

Unique booking status ID.

descriptionstring

Booking status description. E.g. "Active".

+ + + + +

Validated Fields

+ + + + + + + + + + + + + + + +
FieldValidations
description +
    + +
  • string_not_empty: true
  • + +
  • length_lt: 255
  • + +
+
+ + + + + +

BookingStatusID

+

Represents the booking status ID.

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idint32

Unique booking status ID.

+ + + + + +

EmptyBookingMessage

+

An empty message for testing

+ + + + + + + + + + + + + + + diff --git a/examples/doc-multi-page/html/Customer.html b/examples/doc-multi-page/html/Customer.html new file mode 100644 index 00000000..3ebbee98 --- /dev/null +++ b/examples/doc-multi-page/html/Customer.html @@ -0,0 +1,453 @@ + + + + + Protocol Documentation + + + + + + + + + + +

Customer.proto

+

This file has messages for describing a customer.

+ +

Table of Contents

+ +
+ +
+ + + + +

Services

+ + +

CustomerService

+

Customer centric booking service.

+ + + + + + + + + + + + + + + + + + + + + +
Method NameRequest TypeResponse TypeDescription
FindBookingsCustomerBooking stream

Retrieve all bookings made by a customer.

VerifyBookingStatusVerificationRequestBookingStatusMessage

Verify the payment status of a booking.

+ + + + +

Messages

+ + +

Address

+

Represents a mail address.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
address_line_1stringrequired

First address line.

address_line_2stringoptional

Second address line.

address_line_3stringoptional

Second address line.

townstringrequired

Address town.

countystringoptional

Address county, if applicable.

countrystringrequired

Address country.

+ + + + + +

BookingStatusMessage

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
descriptionstringrequired

Booking status description. E.g. "Active".

+ + + + + +

Customer

+

Represents a customer.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idint32required

Unique customer ID.

first_namestringrequired

Customer first name.

last_namestringrequired

Customer last name.

detailsstringoptional

Customer details.

email_addressstringoptional

Customer e-mail address.

phone_numberstringrepeated

Customer phone numbers, primary first.

mail_addressesAddressrepeated

Customer mail addresses, primary first.

+ + + + + +

VerificationRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
customer_idint32required

Unique customer ID.

booking_status_idBookingStatusIDrequired

Unique booking status ID.

+ + + + + + + + + + + + + + + diff --git a/examples/doc-multi-page/html/Vehicle.html b/examples/doc-multi-page/html/Vehicle.html new file mode 100644 index 00000000..b6872617 --- /dev/null +++ b/examples/doc-multi-page/html/Vehicle.html @@ -0,0 +1,502 @@ + + + + + Protocol Documentation + + + + + + + + + + +

Vehicle.proto

+

Messages describing manufacturers / vehicles.

+ +

Table of Contents

+ +
+ +
+ + + + + + + +

Messages

+ + +

Manufacturer

+

Represents a manufacturer of cars.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idint32required

The unique manufacturer ID.

codestringrequired

A manufacturer code, e.g. "DKL4P".

detailsstringoptional

Manufacturer details (minimum orders et.c.).

categoryManufacturer.Categoryoptional

Manufacturer category. Default: CATEGORY_EXTERNAL

+ + + + + +

Model

+

Represents a vehicle model.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstringrequired

The unique model ID.

model_codestringrequired

The car model code, e.g. "PZ003".

model_namestringrequired

The car model name, e.g. "Z3".

daily_hire_rate_dollarssint32required

Dollars per day.

daily_hire_rate_centssint32required

Cents per day.

+ + + + + +

Vehicle

+

Represents a vehicle that can be hired.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idint32required

Unique vehicle ID.

modelModelrequired

Vehicle model.

reg_numberstringrequired

Vehicle registration number.

mileagesint32optional

Current vehicle mileage, if known.

categoryVehicle.Categoryoptional

Vehicle category.

daily_hire_rate_dollarssint32optional

Dollars per day. Default: 50

daily_hire_rate_centssint32optional

Cents per day.

+ + + + +
+ + + + + + + + + + + + + + + +
ExtensionTypeBaseNumberDescription
seriesstringModel100

Vehicle model series.

+ + +

Vehicle.Category

+

Represents a vehicle category. E.g. "Sedan" or "Truck".

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
codestringrequired

Category code. E.g. "S".

descriptionstringrequired

Category name. E.g. "Sedan".

+ + + + + + + + +

Manufacturer.Category

+

Manufacturer category. A manufacturer may be either inhouse or external.

+ + + + + + + + + + + + + + + + + + + +
NameNumberDescription
CATEGORY_INHOUSE0

The manufacturer is inhouse.

CATEGORY_EXTERNAL1

The manufacturer is external.

+ + + +

File-level Extensions

+ + + + + + + + + + + + + + + +
ExtensionTypeBaseNumberDescription
countrystringManufacturer100

Manufacturer country. Default: China

+ + + + + + diff --git a/examples/doc/example.docbook b/examples/doc/example.docbook index dbf34ace..89fde1bb 100644 --- a/examples/doc/example.docbook +++ b/examples/doc/example.docbook @@ -278,6 +278,41 @@ + + +
+ BookingStatusMessage + + + + <classname>BookingStatusMessage</classname> Fields + + + + + + + + Field + Type + Label + Description + + + + + + description + string + required + Booking status description. E.g. "Active". + + + + +
+ +
@@ -357,11 +392,92 @@
+
+ VerificationRequest + + + + <classname>VerificationRequest</classname> Fields + + + + + + + + Field + Type + Label + Description + + + + + + customer_id + int32 + required + Unique customer ID. + + + + booking_status_id + BookingStatusID + required + Unique booking status ID. + + + + +
+ + +
+ +
+ CustomerService + Customer centric booking service. + + <classname>CustomerService</classname> Methods + + + + + + + + Method Name + Request Type + Response Type + Description + + + + + + FindBookings + Customer + Booking stream + Retrieve all bookings made by a customer. + + + + VerifyBookingStatus + VerificationRequest + BookingStatusMessage + Verify the payment status of a booking. + + + + +
+
+
diff --git a/examples/doc/example.html b/examples/doc/example.html index 96e463bb..c092dd72 100644 --- a/examples/doc/example.html +++ b/examples/doc/example.html @@ -213,13 +213,25 @@

Table of Contents

MAddress +
  • + MBookingStatusMessage +
  • +
  • MCustomer
  • +
  • + MVerificationRequest +
  • + +
  • + SCustomerService +
  • + @@ -568,6 +580,30 @@

    Address

    +

    BookingStatusMessage

    +

    + + + + + + + + + + + + + + + + +
    FieldTypeLabelDescription
    descriptionstringrequired

    Booking status description. E.g. "Active".

    + + + + +

    Customer

    Represents a customer.

    @@ -634,12 +670,69 @@

    Customer

    +

    VerificationRequest

    +

    + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeLabelDescription
    customer_idint32required

    Unique customer ID.

    booking_status_idBookingStatusIDrequired

    Unique booking status ID.

    + + + + + +

    CustomerService

    +

    Customer centric booking service.

    + + + + + + + + + + + + + + + + + + + + + +
    Method NameRequest TypeResponse TypeDescription
    FindBookingsCustomerBooking stream

    Retrieve all bookings made by a customer.

    VerifyBookingStatusVerificationRequestBookingStatusMessage

    Verify the payment status of a booking.

    + +
    diff --git a/examples/doc/example.json b/examples/doc/example.json index 08486ccd..07f4bd47 100644 --- a/examples/doc/example.json +++ b/examples/doc/example.json @@ -28,6 +28,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -40,6 +41,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -52,6 +54,7 @@ "type": "BookingStatus", "longType": "BookingStatus", "fullType": "com.example.BookingStatus", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -64,6 +67,7 @@ "type": "bool", "longType": "bool", "fullType": "bool", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -76,6 +80,7 @@ "type": "bool", "longType": "bool", "fullType": "bool", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -88,6 +93,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -115,6 +121,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -127,6 +134,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -163,6 +171,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -195,10 +204,12 @@ "requestType": "Booking", "requestLongType": "Booking", "requestFullType": "com.example.Booking", + "requestTypeFile": "", "requestStreaming": false, "responseType": "BookingStatus", "responseLongType": "BookingStatus", "responseFullType": "com.example.BookingStatus", + "responseTypeFile": "", "responseStreaming": false, "options": { "google.api.http": { @@ -218,10 +229,12 @@ "requestType": "BookingStatusID", "requestLongType": "BookingStatusID", "requestFullType": "com.example.BookingStatusID", + "requestTypeFile": "", "requestStreaming": false, "responseType": "BookingStatus", "responseLongType": "BookingStatus", "responseFullType": "com.example.BookingStatus", + "responseTypeFile": "", "responseStreaming": true } ] @@ -235,7 +248,7 @@ "hasEnums": false, "hasExtensions": false, "hasMessages": true, - "hasServices": false, + "hasServices": true, "enums": [], "extensions": [], "messages": [ @@ -256,6 +269,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -268,6 +282,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -280,6 +295,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -292,6 +308,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -304,6 +321,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -316,6 +334,32 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, + { + "name": "BookingStatusMessage", + "longName": "BookingStatusMessage", + "fullName": "com.example.BookingStatusMessage", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "description", + "description": "Booking status description. E.g. \"Active\".", + "label": "required", + "type": "string", + "longType": "string", + "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -340,6 +384,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -352,6 +397,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -364,6 +410,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -376,6 +423,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -388,6 +436,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -400,6 +449,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -412,6 +462,45 @@ "type": "Address", "longType": "Address", "fullType": "com.example.Address", + "typeFile": "", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, + { + "name": "VerificationRequest", + "longName": "VerificationRequest", + "fullName": "com.example.VerificationRequest", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "customer_id", + "description": "Unique customer ID.", + "label": "required", + "type": "int32", + "longType": "int32", + "fullType": "int32", + "typeFile": "", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "booking_status_id", + "description": "Unique booking status ID.", + "label": "required", + "type": "BookingStatusID", + "longType": "BookingStatusID", + "fullType": "com.example.BookingStatusID", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -420,7 +509,44 @@ ] } ], - "services": [] + "services": [ + { + "name": "CustomerService", + "longName": "CustomerService", + "fullName": "com.example.CustomerService", + "description": "Customer centric booking service.", + "methods": [ + { + "name": "FindBookings", + "description": "Retrieve all bookings made by a customer.", + "requestType": "Customer", + "requestLongType": "Customer", + "requestFullType": "com.example.Customer", + "requestTypeFile": "", + "requestStreaming": false, + "responseType": "Booking", + "responseLongType": "Booking", + "responseFullType": "com.example.Booking", + "responseTypeFile": "", + "responseStreaming": true + }, + { + "name": "VerifyBookingStatus", + "description": "Verify the payment status of a booking.", + "requestType": "VerificationRequest", + "requestLongType": "VerificationRequest", + "requestFullType": "com.example.VerificationRequest", + "requestTypeFile": "", + "requestStreaming": false, + "responseType": "BookingStatusMessage", + "responseLongType": "BookingStatusMessage", + "responseFullType": "com.example.BookingStatusMessage", + "responseTypeFile": "", + "responseStreaming": false + } + ] + } + ] }, { "name": "Vehicle.proto", @@ -485,6 +611,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -497,6 +624,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -509,6 +637,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -521,6 +650,7 @@ "type": "Category", "longType": "Manufacturer.Category", "fullType": "com.example.Manufacturer.Category", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -545,6 +675,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -557,6 +688,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -569,6 +701,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -581,6 +714,7 @@ "type": "sint32", "longType": "sint32", "fullType": "sint32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -593,6 +727,7 @@ "type": "sint32", "longType": "sint32", "fullType": "sint32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -636,6 +771,7 @@ "type": "int32", "longType": "int32", "fullType": "int32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -648,6 +784,7 @@ "type": "Model", "longType": "Model", "fullType": "com.example.Model", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -660,6 +797,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -672,6 +810,7 @@ "type": "sint32", "longType": "sint32", "fullType": "sint32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -684,6 +823,7 @@ "type": "Category", "longType": "Vehicle.Category", "fullType": "com.example.Vehicle.Category", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -696,6 +836,7 @@ "type": "sint32", "longType": "sint32", "fullType": "sint32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -708,6 +849,7 @@ "type": "sint32", "longType": "sint32", "fullType": "sint32", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -732,6 +874,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", @@ -744,6 +887,7 @@ "type": "string", "longType": "string", "fullType": "string", + "typeFile": "", "ismap": false, "isoneof": false, "oneofdecl": "", diff --git a/examples/doc/example.md b/examples/doc/example.md index 15b5bf35..6b7d7e4c 100644 --- a/examples/doc/example.md +++ b/examples/doc/example.md @@ -13,7 +13,11 @@ - [Customer.proto](#Customer-proto) - [Address](#com-example-Address) + - [BookingStatusMessage](#com-example-BookingStatusMessage) - [Customer](#com-example-Customer) + - [VerificationRequest](#com-example-VerificationRequest) + + - [CustomerService](#com-example-CustomerService) - [Vehicle.proto](#Vehicle-proto) - [Manufacturer](#com-example-Manufacturer) @@ -149,6 +153,21 @@ Represents a mail address. + + +### BookingStatusMessage + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| description | [string](#string) | required | Booking status description. E.g. "Active". | + + + + + + ### Customer @@ -169,12 +188,39 @@ Represents a customer. + + + +### VerificationRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| customer_id | [int32](#int32) | required | Unique customer ID. | +| booking_status_id | [BookingStatusID](#com-example-BookingStatusID) | required | Unique booking status ID. | + + + + + + + + +### CustomerService +Customer centric booking service. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| FindBookings | [Customer](#com-example-Customer) | [Booking](#com-example-Booking) stream | Retrieve all bookings made by a customer. | +| VerifyBookingStatus | [VerificationRequest](#com-example-VerificationRequest) | [BookingStatusMessage](#com-example-BookingStatusMessage) | Verify the payment status of a booking. | + diff --git a/examples/doc/example.txt b/examples/doc/example.txt index db07a128..babd070d 100644 --- a/examples/doc/example.txt +++ b/examples/doc/example.txt @@ -96,6 +96,19 @@ Represents a mail address. +=== BookingStatusMessage + + + +|=========================================== +|*Field* |*Type* |*Label* |*Description* + +|description | <> |required |Booking status description. E.g. "Active". + +|=========================================== + + + === Customer Represents a customer. @@ -121,6 +134,21 @@ Represents a customer. +=== VerificationRequest + + + +|=========================================== +|*Field* |*Type* |*Label* |*Description* + +|customer_id | <> |required |Unique customer ID. + +|booking_status_id | <> |required |Unique booking status ID. + +|=========================================== + + + diff --git a/examples/proto/Customer.proto b/examples/proto/Customer.proto index d640ac29..a0249801 100644 --- a/examples/proto/Customer.proto +++ b/examples/proto/Customer.proto @@ -2,6 +2,7 @@ syntax = "proto2"; import "github.com/envoyproxy/protoc-gen-validate/validate/validate.proto"; +import "Booking.proto"; package com.example; @@ -35,3 +36,23 @@ message Customer { repeated string phone_number = 6; /// Customer phone numbers, primary first. repeated Address mail_addresses = 7; /// Customer mail addresses, primary first. } + +message VerificationRequest { + required int32 customer_id = 1; // Unique customer ID. + required BookingStatusID booking_status_id = 2; // Unique booking status ID. +} + +message BookingStatusMessage { + required string description = 2; /// Booking status description. E.g. "Active". +} + +/** + * Customer centric booking service. + */ +service CustomerService { + /// Retrieve all bookings made by a customer. + rpc FindBookings (Customer) returns (stream Booking); + + /// Verify the payment status of a booking. + rpc VerifyBookingStatus(VerificationRequest) returns (BookingStatusMessage); +} diff --git a/examples/templates/html-multi-page.tmpl b/examples/templates/html-multi-page.tmpl new file mode 100644 index 00000000..a146f3f2 --- /dev/null +++ b/examples/templates/html-multi-page.tmpl @@ -0,0 +1,440 @@ + + + + + Protocol Documentation + + + + + + + + + + {{range .Files}} +

    {{.Name}}

    + {{p .Description}} + +

    Table of Contents

    + +
    + +
    + {{end}} + + {{range .Files}} + {{if .HasServices}} +

    Services

    + {{end}} + {{range .Services}} +

    {{.Name}}

    + {{p .Description}} + + + + + + {{range .Methods}} + + + + + + + {{end}} + +
    Method NameRequest TypeResponse TypeDescription
    {{.Name}}{{.RequestLongType}}{{if .RequestStreaming}} stream{{end}}{{.ResponseLongType}}{{if .ResponseStreaming}} stream{{end}}

    {{.Description}}

    + + {{$service := .}} + {{- range .MethodOptions}} + {{$option := .}} + {{if eq . "google.api.http"}} +

    Methods with HTTP bindings

    + + + + + + + + + + + {{range $service.MethodsWithOption .}} + {{$name := .Name}} + {{range (.Option $option).Rules}} + + + + + + + {{end}} + {{end}} + +
    Method NameMethodPatternBody
    {{$name}}{{.Method}}{{.Pattern}}{{.Body}}
    + {{else}} +

    Methods with {{.}} option

    + + + + + + + + + {{range $service.MethodsWithOption .}} + + + + + {{end}} + +
    Method NameOption
    {{.Name}}

    {{ printf "%+v" (.Option $option)}}

    + {{end}} + {{end -}} + {{end}} + + {{if .HasMessages}} +

    Messages

    + {{end}} + {{range .Messages}} +

    {{.LongName}}

    + {{p .Description}} + + {{if .HasFields}} + + + + + + {{range .Fields}} + + + + + + + {{end}} + +
    FieldTypeLabelDescription
    {{.Name}}{{.LongType}}{{.Label}}

    {{if (index .Options "deprecated"|default false)}}Deprecated. {{end}}{{.Description}} {{if .DefaultValue}}Default: {{.DefaultValue}}{{end}}

    + + {{$message := .}} + {{- range .FieldOptions}} + {{$option := .}} + {{if eq . "validator.field" "validate.rules" }} +

    Validated Fields

    + + + + + + + + + {{range $message.FieldsWithOption .}} + + + + + {{end}} + +
    FieldValidations
    {{.Name}} +
      + {{range (.Option $option).Rules}} +
    • {{.Name}}: {{.Value}}
    • + {{end}} +
    +
    + {{else}} +

    Fields with {{.}} option

    + + + + + + + + + {{range $message.FieldsWithOption .}} + + + + + {{end}} + +
    NameOption
    {{.Name}}

    {{ printf "%+v" (.Option $option)}}

    + {{end}} + {{end -}} + {{end}} + + {{if .HasExtensions}} +
    + + + + + + {{range .Extensions}} + + + + + + + + {{end}} + +
    ExtensionTypeBaseNumberDescription
    {{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

    {{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

    + {{end}} + {{end}} + + {{if .HasEnums}} +

    Enums

    + {{end}} + {{range .Enums}} +

    {{.LongName}}

    + {{p .Description}} + + + + + + {{range .Values}} + + + + + + {{end}} + +
    NameNumberDescription
    {{.Name}}{{.Number}}

    {{.Description}}

    + {{end}} + + {{if .HasExtensions}} +

    File-level Extensions

    + + + + + + {{range .Extensions}} + + + + + + + + {{end}} + +
    ExtensionTypeBaseNumberDescription
    {{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

    {{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

    + {{end}} + + {{end}} + + + diff --git a/plugin.go b/plugin.go index bc769016..dcc72b9e 100644 --- a/plugin.go +++ b/plugin.go @@ -21,6 +21,7 @@ type PluginOptions struct { OutputFile string ExcludePatterns []*regexp.Regexp SourceRelative bool + SeparateFiles bool } // SupportedFeatures describes a flag setting for supported features. @@ -51,19 +52,42 @@ func (p *Plugin) Generate(r *plugin_go.CodeGeneratorRequest) (*plugin_go.CodeGen } resp := new(plugin_go.CodeGeneratorResponse) + fdsGroup := groupProtosByDirectory(result, options.SourceRelative) for dir, fds := range fdsGroup { - template := NewTemplate(fds) + if !options.SeparateFiles { + template := NewTemplate(fds) - output, err := RenderTemplate(options.Type, template, customTemplate) - if err != nil { - return nil, err - } + output, err := RenderTemplate(options.Type, template, customTemplate) + if err != nil { + return nil, err + } + + resp.File = append(resp.File, &plugin_go.CodeGeneratorResponse_File{ + Name: proto.String(filepath.Join(dir, options.OutputFile)), + Content: proto.String(string(output)), + }) + } else { + for _, fd := range fds { + template := NewTemplate([]*protokit.FileDescriptor{fd}) - resp.File = append(resp.File, &plugin_go.CodeGeneratorResponse_File{ - Name: proto.String(filepath.Join(dir, options.OutputFile)), - Content: proto.String(string(output)), - }) + ResolveTypePaths(template) + + output, err := RenderTemplate(options.Type, template, customTemplate) + if err != nil { + return nil, err + } + + _, protoFilename := filepath.Split(fd.GetName()) + + filenameParts := strings.Split(protoFilename, ".") + + resp.File = append(resp.File, &plugin_go.CodeGeneratorResponse_File{ + Name: proto.String(filepath.Join(dir, filenameParts[0]+"."+options.OutputFile)), + Content: proto.String(string(output)), + }) + } + } } resp.SupportedFeatures = proto.Uint64(SupportedFeatures) @@ -107,7 +131,7 @@ OUTER: // ParseOptions parses plugin options from a CodeGeneratorRequest. It does this by splitting the `Parameter` field from // the request object and parsing out the type of renderer to use and the name of the file to be generated. // -// The parameter (`--doc_opt`) must be of the format ,[,default|source_relative]:,*. +// The parameter (`--doc_opt`) must be of the format ,[,default|source_relative][,default|separate_files]:,*. // The file will be written to the directory specified with the `--doc_out` argument to protoc. func ParseOptions(req *plugin_go.CodeGeneratorRequest) (*PluginOptions, error) { options := &PluginOptions{ @@ -115,6 +139,7 @@ func ParseOptions(req *plugin_go.CodeGeneratorRequest) (*PluginOptions, error) { TemplateFile: "", OutputFile: "index.html", SourceRelative: false, + SeparateFiles: false, } params := req.GetParameter() @@ -140,23 +165,40 @@ func ParseOptions(req *plugin_go.CodeGeneratorRequest) (*PluginOptions, error) { } parts := strings.Split(params, ",") - if len(parts) < 2 || len(parts) > 3 { + if len(parts) < 2 || len(parts) > 4 { return nil, fmt.Errorf("Invalid parameter: %s", params) } options.TemplateFile = parts[0] options.OutputFile = path.Base(parts[1]) + + // Handle extra options if len(parts) > 2 { - switch parts[2] { - case "source_relative": - options.SourceRelative = true - case "default": - options.SourceRelative = false - default: - return nil, fmt.Errorf("Invalid parameter: %s", params) + extraOptions := parts[2:] + + for i := range extraOptions { + switch i { + case 0: // Third option + switch extraOptions[i] { + case "source_relative": + options.SourceRelative = true + case "default": + options.SourceRelative = false + default: + return nil, fmt.Errorf("Invalid parameter: %s", params) + } + case 1: // Fourth option + switch extraOptions[i] { + case "separate_files": + options.SeparateFiles = true + case "default": + options.SeparateFiles = false + default: + return nil, fmt.Errorf("Invalid parameter: %s", params) + } + } } } - options.SourceRelative = len(parts) > 2 && parts[2] == "source_relative" renderType, err := NewRenderType(options.TemplateFile) if err == nil { diff --git a/plugin_test.go b/plugin_test.go index ebb18b7d..2d877dcf 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -53,6 +53,24 @@ func TestParseOptionsForSourceRelative(t *testing.T) { require.Equal(t, options.SourceRelative, false) } +func TestParseOptionsForSeparateFiles(t *testing.T) { + req := new(plugin_go.CodeGeneratorRequest) + req.Parameter = proto.String("markdown,index.md,source_relative,separate_files") + options, err := ParseOptions(req) + require.NoError(t, err) + require.Equal(t, options.SeparateFiles, true) + + req.Parameter = proto.String("markdown,index.md,source_relative,default") + options, err = ParseOptions(req) + require.NoError(t, err) + require.Equal(t, options.SeparateFiles, false) + + req.Parameter = proto.String("markdown,index.md,source_relative") + options, err = ParseOptions(req) + require.NoError(t, err) + require.Equal(t, options.SeparateFiles, false) +} + func TestParseOptionsForCustomTemplate(t *testing.T) { req := new(plugin_go.CodeGeneratorRequest) req.Parameter = proto.String("/path/to/template.tmpl,/base/name/only/output.md") @@ -150,3 +168,47 @@ func TestRunPluginForSourceRelative(t *testing.T) { require.NotEmpty(t, resp.File[0].GetContent()) require.NotEmpty(t, resp.File[1].GetContent()) } + +func TestRunPluginForSeparateFilesSourceRelative(t *testing.T) { + set, _ := utils.LoadDescriptorSet("fixtures", "fileset.pb") + req := utils.CreateGenRequest(set, "Booking.proto", "Vehicle.proto", "nested/Book.proto") + req.Parameter = proto.String("markdown,md,source_relative,separate_files") + + plugin := new(Plugin) + resp, err := plugin.Generate(req) + require.NoError(t, err) + require.Len(t, resp.File, 3) + expected := map[string]int{"Booking.md": 1, "Vehicle.md": 1, "nested/Book.md": 1} + require.Contains(t, expected, resp.File[0].GetName()) + delete(expected, resp.File[0].GetName()) + require.Contains(t, expected, resp.File[1].GetName()) + delete(expected, resp.File[1].GetName()) + require.Contains(t, expected, resp.File[2].GetName()) + delete(expected, resp.File[2].GetName()) + + require.NotEmpty(t, resp.File[0].GetContent()) + require.NotEmpty(t, resp.File[1].GetContent()) + require.NotEmpty(t, resp.File[2].GetContent()) +} + +func TestRunPluginForSeparateFilesNoSourceRelative(t *testing.T) { + set, _ := utils.LoadDescriptorSet("fixtures", "fileset.pb") + req := utils.CreateGenRequest(set, "Booking.proto", "Vehicle.proto", "nested/Book.proto") + req.Parameter = proto.String("markdown,md,default,separate_files") + + plugin := new(Plugin) + resp, err := plugin.Generate(req) + require.NoError(t, err) + require.Len(t, resp.File, 3) + expected := map[string]int{"Booking.md": 1, "Vehicle.md": 1, "Book.md": 1} + require.Contains(t, expected, resp.File[0].GetName()) + delete(expected, resp.File[0].GetName()) + require.Contains(t, expected, resp.File[1].GetName()) + delete(expected, resp.File[1].GetName()) + require.Contains(t, expected, resp.File[2].GetName()) + delete(expected, resp.File[2].GetName()) + + require.NotEmpty(t, resp.File[0].GetContent()) + require.NotEmpty(t, resp.File[1].GetContent()) + require.NotEmpty(t, resp.File[2].GetContent()) +} diff --git a/template.go b/template.go index 9edaac72..12d2a836 100644 --- a/template.go +++ b/template.go @@ -20,6 +20,8 @@ type Template struct { Scalars []*ScalarValue `json:"scalarValueTypes"` } +var typesMap = make(map[string]string) + // NewTemplate creates a Template object from a set of descriptors. func NewTemplate(descs []*protokit.FileDescriptor) *Template { files := make([]*File, 0, len(descs)) @@ -42,6 +44,7 @@ func NewTemplate(descs []*protokit.FileDescriptor) *Template { for _, e := range f.Enums { file.Enums = append(file.Enums, parseEnum(e)) + typesMap[e.GetFullName()] = fileNameToMapValue(file.Name) } for _, e := range f.Extensions { @@ -52,8 +55,10 @@ func NewTemplate(descs []*protokit.FileDescriptor) *Template { var addFromMessage func(*protokit.Descriptor) addFromMessage = func(m *protokit.Descriptor) { file.Messages = append(file.Messages, parseMessage(m)) + typesMap[m.GetFullName()] = fileNameToMapValue(file.Name) for _, e := range m.Enums { file.Enums = append(file.Enums, parseEnum(e)) + typesMap[e.GetFullName()] = fileNameToMapValue(file.Name) } for _, n := range m.Messages { addFromMessage(n) @@ -78,6 +83,30 @@ func NewTemplate(descs []*protokit.FileDescriptor) *Template { return &Template{Files: files, Scalars: makeScalars()} } +func fileNameToMapValue(fileName string) string { + return strings.TrimSuffix(fileName, ".proto") +} + +func ResolveTypePaths(tmpl *Template) { + for _, file := range tmpl.Files { + for _, service := range file.Services { + for i, method := range service.Methods { + requestFile := typesMap[method.RequestFullType] + service.Methods[i].RequestTypeFile = requestFile + + responseFile := typesMap[method.ResponseFullType] + service.Methods[i].ResponseTypeFile = responseFile + } + } + for _, message := range file.Messages { + for i, field := range message.Fields { + file := typesMap[field.FullType] + message.Fields[i].TypeFile = file + } + } + } +} + func makeScalars() []*ScalarValue { var scalars []*ScalarValue json.Unmarshal(scalarsJSON, &scalars) @@ -230,6 +259,7 @@ type MessageField struct { Type string `json:"type"` LongType string `json:"longType"` FullType string `json:"fullType"` + TypeFile string `json:"typeFile"` IsMap bool `json:"ismap"` IsOneof bool `json:"isoneof"` OneofDecl string `json:"oneofdecl"` @@ -365,10 +395,12 @@ type ServiceMethod struct { RequestType string `json:"requestType"` RequestLongType string `json:"requestLongType"` RequestFullType string `json:"requestFullType"` + RequestTypeFile string `json:"requestTypeFile"` RequestStreaming bool `json:"requestStreaming"` ResponseType string `json:"responseType"` ResponseLongType string `json:"responseLongType"` ResponseFullType string `json:"responseFullType"` + ResponseTypeFile string `json:"responseTypeFile"` ResponseStreaming bool `json:"responseStreaming"` Options map[string]interface{} `json:"options,omitempty"` @@ -479,6 +511,7 @@ func parseMessageField(pf *protokit.FieldDescriptor, oneofDecls []*descriptor.On Type: t, LongType: lt, FullType: ft, + TypeFile: "", DefaultValue: pf.GetDefaultValue(), Options: mergeOptions(extractOptions(pf.GetOptions()), extensions.Transform(pf.OptionExtensions)), IsOneof: pf.OneofIndex != nil, @@ -525,10 +558,12 @@ func parseServiceMethod(pm *protokit.MethodDescriptor) *ServiceMethod { RequestType: baseName(pm.GetInputType()), RequestLongType: strings.TrimPrefix(pm.GetInputType(), "."+pm.GetPackage()+"."), RequestFullType: strings.TrimPrefix(pm.GetInputType(), "."), + RequestTypeFile: "", RequestStreaming: pm.GetClientStreaming(), ResponseType: baseName(pm.GetOutputType()), ResponseLongType: strings.TrimPrefix(pm.GetOutputType(), "."+pm.GetPackage()+"."), ResponseFullType: strings.TrimPrefix(pm.GetOutputType(), "."), + ResponseTypeFile: "", ResponseStreaming: pm.GetServerStreaming(), Options: mergeOptions(extractOptions(pm.GetOptions()), extensions.Transform(pm.OptionExtensions)), } diff --git a/template_test.go b/template_test.go index 717919b2..bb8556ae 100644 --- a/template_test.go +++ b/template_test.go @@ -436,6 +436,26 @@ func TestExcludedComments(t *testing.T) { require.Equal(t, "the id of this message.", findField("id", message).Description) } +func TestResolveTypePaths(t *testing.T) { + ResolveTypePaths(template) + + service := findService("BookingService", bookingFile) + method := findServiceMethod("BookVehicle", service) + require.Equal(t, "Booking", method.RequestTypeFile) + require.Equal(t, "Booking", method.ResponseTypeFile) + message := findMessage("Booking", bookingFile) + require.Equal(t, "Booking", findField("status", message).TypeFile) + // ensure literal types have no TypeFile + require.Empty(t, findField("vehicle_id", message).TypeFile) + + service = findService("VehicleService", vehicleFile) + method = findServiceMethod("GetModels", service) + require.Equal(t, "Vehicle", method.RequestTypeFile) + require.Equal(t, "Vehicle", method.ResponseTypeFile) + message = findMessage("Manufacturer", vehicleFile) + require.Equal(t, "Vehicle", findField("category", message).TypeFile) +} + func findService(name string, f *File) *Service { for _, s := range f.Services { if s.Name == name {