ezcx

package module
v0.0.0-...-ac68d48 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2022 License: Apache-2.0 Imports: 15 Imported by: 4

README

ezcx

ezcx is a framework for building containerized Dialogflow CX webhook fulfillment APIs. ezcx runs happiest on Google's Cloud Run service.

ezcx was designed to remove most (if not all) the complexity associated with building Dialogflow CX webhook fulfillment APIs:

  • ezcx is a convenience wrapper on top of Google Cloud's code-generated gRPC definitions and exposes wrappers around the WebhookResponse, WebhookRequest, and subsequent protobuf messages used in defining the WebhookRequest and WebhookResponse.

  • ezcx makes it easy to add WebhookResponse response messages. The WebhookResponse object has a number of helper methods like AddTextResponse, AddOutputAudioTextResponse and AddSessionParameters that circumvent the need to manage sub-object allocation (forgot to make that map? PANIC!), programming labor, and the absolute headache of keeping track of deeply nested objects.

func CxHandler(res *ezcx.WebhookResponse, req *ezcx.WebhookRequest) error {
	// Add text
    res.AddTextResponse("Made with ezcx in 5 minutes or less...")
    
    // Add SSML
    res.AddOutputAudioTextResponse("<ssml>Made with ezcx in <prosody rate=slow>5 minutes </prosody>or less...</ssml>")    

    // Add Session Parameters
    params = make(map[string]any)
    params["made_with"] = "ezcx"
    res.AddSessionParameters(params)

	return nil
}
  • ezcx is designed to be a full solution for Dialogflow CX Webhook Fulfillment APIs. You can use ezcx.NewServer to create an http.Server that instance that's wired up to work with functional ezcx.HandlerFunc handlers. Very much like http.HandleFunc, ezcx.HandlerFunc is an adapter that allows for the definition of CxHandlers of the form func(*WebhookResponse, *WebhookRequest) error to be directly used via the server's HandleCx method.
type HandlerFunc func(*WebhookResponse, *WebhookRequest) error

// Ignore for now.
func (h HandlerFunc) Handle(res *WebhookResponse, req *WebhookRequest) error {
	return h(res, req)
}

// Implements the http.Handler interface - this is relatively low level;
// ezcx "handles" this for you, instead, allowing you to focus on what really matters:
// working with the data in WebhookRequest and returning a WebhookResponse.
func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	req, err := WebhookRequestFromRequest(r)
	if err != nil {
		log.Println(err)
		return
	}
	res := req.PrepareResponse()
	err = h.Handle(res, req)
	if err != nil {
		log.Println(err)
		return
	}
	res.WriteResponse(w)
}
  • Creating a web service with ezcx couldn't be easier. the ezcx.Server object most (if not all) the features you'd need from a production http.Server instance: signal handling, logging, and graceful shutdowns. If you see an opportuntiy for improvement, please reach out!
// main.go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/yaq-cc/ezcx"
)

func main() {
    parent := context.Background()
    lg := log.Default()

    server := ezcx.NewServer(parent, ":8082", lg)
    // HandleCx adapts ezcx.HandlerFunc into an http.Handler for you!
    server.HandleCx("/from-dfcx", CxHandler)
    server.ListenAndServe(parent)
}

Handlers have been moved to a separate file to show just how little effort is required. server.HandleCx adapts an ezcx.HandlerFunc into an http.Handler for you!

// handlers.go
func CxHandler(res *ezcx.WebhookResponse, req *ezcx.WebhookRequest) error {
	// Read parameters.
	params, err := req.GetSessionParameters()
	if err != nil {
		return err
	}
	callerName := params["caller-name"]

	// Update your response.
	res.AddTextResponse(fmt.Sprintf("Hi there %s, how are you?", callerName))

	// Update some session parameters
	params["saidHi"] = true
	err = res.AddSessionParameters(params)
	if err != nil {
		return err
	}

	return nil
}

Basic Usage

Request-scoped Web Service Calls.

ezcx's WebhookRequest flows down http.Request's context; this context is accessible via the WebhookRequest's Context() method. Under the hood, WebhookRequests.Context() method is just a pass-through for (*http.Request).Context().

func CxHandler(res *ezcx.WebhookResponse, req *ezcx.WebhookRequest) error {
  ...
  ctx := req.Context() 
  apiResult, err := makeWebServiceCall(ctx, ...callOpts)
  if err != nil {
    return err
  }
  ...
	return nil
}

Testing

More on testing coming soon!

Examples

Please visit the examples folder to check out how ezcx stacks up!

Dockerfile

Provided for convenience.

FROM    golang:1.18-buster as builder
WORKDIR /app
COPY    . ./
RUN     go build -o service

FROM    debian:buster-slim
RUN     set -x && \
		apt-get update && \
		DEBIAN_FRONTEND=noninteractive apt-get install -y \
			ca-certificates && \
			rm -rf /var/lib/apt/lists/*
COPY    --from=builder /app/service /app/service

CMD     ["/app/service"]

Cloud Build

Provided for convenience. Review all the parameters for deploying to Cloud Run before issuing a gcloud builds submit!

steps:
- id: docker-build-push-ezcx-service
  waitFor: ['-']
  name: gcr.io/cloud-builders/docker
  dir: service
  entrypoint: bash
  args:
    - -c
    - |
      docker build -t gcr.io/$PROJECT_ID/${_SERVICE} . &&
      docker push gcr.io/$PROJECT_ID/${_SERVICE}

- id: gcloud-run-deploy-ezcx-service
  waitFor: ['docker-build-push-ezcx-service']
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: bash
  args:
    - -c
    - |
      gcloud run deploy ${_SERVICE} \
        --project $PROJECT_ID \
        --image gcr.io/$PROJECT_ID/${_SERVICE} \
        --timeout 5m \
        --region ${_REGION} \
        --no-cpu-throttling \
        --min-instances 0 \
        --max-instances 3 \
        --allow-unauthenticated

substitutions:
  _SERVICE: ezcx-service
  _REGION: us-central-1

Backlog

Testing

EmptyWebhookRequest

Review the initialization of emptyWebhookRequest which is used for testing webhooks. I need to add a place for "pageInfo" which is an object that's rarely used. Details on pageInfo here: https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/dialogflow/cx/v3#PageInfo

Updates

  • 2022-10-07: WebhookRequest now has a method that returns the http.Request's context. Adding in a Context() method was the simplest and most effective way of providing a request-scoped context to downstream web service calls.

Documentation

Index

Constants

View Source
const (
	Logger contextKey = iota
)

Variables

View Source
var (
	ServerDefaultSignals []os.Signal = []os.Signal{
		syscall.SIGINT,
		syscall.SIGTERM,
		syscall.SIGHUP,
	}
)

Functions

func ErrUnmarshalWrapper

func ErrUnmarshalWrapper(site string, err error) error

Types

type HandlerFunc

type HandlerFunc func(*WebhookResponse, *WebhookRequest) error

HandlerFunc is an adapter that converts a given ezcx.HandlerFunc into an http.Handler.

func (HandlerFunc) ServeHTTP

func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)

Implementing ServeHTTP allows the ezcx.HandlerFunc to satisfy the http.Handler interface.

Error handling is an area of future improvement. For instance, if a required parameter is missing, it should be up to the developer to handle that i.e.: return an HTTP error (400, 500) or return a ResponseMessage indicating something went wrong...

type Server

type Server struct {
	// contains filtered or unexported fields
}

func NewServer

func NewServer(ctx context.Context, addr string, lg *log.Logger, signals ...os.Signal) *Server

func (*Server) HandleCx

func (s *Server) HandleCx(pattern string, handler HandlerFunc)

HandleCx registers the handler for the given pattern. While the HandleCx method itself isn't safe for concurrent usage, the underlying method it wraps (*ServeMux).Handle IS guarded by a mutex.

func (*Server) Init

func (s *Server) Init(ctx context.Context, addr string, lg *log.Logger, signals ...os.Signal) *Server

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context)

ListenAndServe listens on the TCP network address srv.Addr and then calls Serve to handle requests on incoming connections. ListenAndServe is responsible for handling signals and managing graceful shutdown(s) whenever the right signals are intercepted.

func (*Server) Reconfigure

func (s *Server) Reconfigure() error

Omitted for now.

func (*Server) ServeMux

func (s *Server) ServeMux() *http.ServeMux

ServeMux returns a copy of the currently set mux.

func (*Server) SetHandler

func (s *Server) SetHandler(h http.Handler)

SetHandler allows the user to set a custom mux or handler.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown provides graceful shutdown for the entire ezcx Server

type WebhookRequest

type WebhookRequest struct {
	cx.WebhookRequest
	// contains filtered or unexported fields
}

func NewTestingWebhookRequest

func NewTestingWebhookRequest(session, payload, pageform map[string]any) (*WebhookRequest, error)

Testing

func NewWebhookRequest

func NewWebhookRequest() *WebhookRequest

func WebhookRequestFromReader

func WebhookRequestFromReader(rd io.Reader) (*WebhookRequest, error)

yaquino@2022-10-11: Dialogflow CX API May include "extra" fields that may throw errors and interface with protojson.Unmarshal. As per the documentation, these fields may be ignored. Now also pointing at req.WebhookRequest for unmarshalling..

func WebhookRequestFromRequest

func WebhookRequestFromRequest(r *http.Request) (*WebhookRequest, error)

yaquino@2022-10-07: Refactored to flow http.Request's context to the WebhookRequest instance.

func (*WebhookRequest) Context

func (req *WebhookRequest) Context() context.Context

func (*WebhookRequest) CopyPageInfo

func (req *WebhookRequest) CopyPageInfo(res *WebhookResponse)

func (*WebhookRequest) CopyPayload

func (req *WebhookRequest) CopyPayload(res *WebhookResponse) *WebhookResponse

func (*WebhookRequest) CopySessionInfo

func (req *WebhookRequest) CopySessionInfo(res *WebhookResponse) *WebhookResponse

func (*WebhookRequest) GetPageFormParameters

func (req *WebhookRequest) GetPageFormParameters() map[string]any

func (*WebhookRequest) GetPayload

func (req *WebhookRequest) GetPayload() map[string]any

func (*WebhookRequest) GetPayloadParameter

func (req *WebhookRequest) GetPayloadParameter(key string) (any, bool)

func (*WebhookRequest) GetSessionParameter

func (req *WebhookRequest) GetSessionParameter(key string) (any, bool)

func (*WebhookRequest) GetSessionParameters

func (req *WebhookRequest) GetSessionParameters() map[string]any

func (*WebhookRequest) InitializeResponse

func (req *WebhookRequest) InitializeResponse() *WebhookResponse

func (*WebhookRequest) Logger

func (req *WebhookRequest) Logger() *log.Logger

.

func (*WebhookRequest) ReadReader

func (req *WebhookRequest) ReadReader(rd io.Reader) error

func (*WebhookRequest) ReadRequest

func (req *WebhookRequest) ReadRequest(r *http.Request) error

func (*WebhookRequest) TestCxHandler

func (req *WebhookRequest) TestCxHandler(out io.Writer, h HandlerFunc) (*WebhookResponse, error)

yaquino: 2022-10-08Review this...!

func (*WebhookRequest) WriteRequest

func (req *WebhookRequest) WriteRequest(w io.Writer) error

Is this the right format??

type WebhookResponse

type WebhookResponse struct {
	cx.WebhookResponse
}

func NewWebhookResponse

func NewWebhookResponse() *WebhookResponse

func (*WebhookResponse) AddOutputAudioTextResponse

func (res *WebhookResponse) AddOutputAudioTextResponse(ssml string)

func (*WebhookResponse) AddPayload

func (res *WebhookResponse) AddPayload(m map[string]any) error

func (*WebhookResponse) AddSessionParameters

func (res *WebhookResponse) AddSessionParameters(m map[string]any) error

func (*WebhookResponse) AddTextResponse

func (res *WebhookResponse) AddTextResponse(txts ...string)

func (*WebhookResponse) SetPayload

func (res *WebhookResponse) SetPayload(m map[string]any) error

func (*WebhookResponse) SetSessionParameters

func (res *WebhookResponse) SetSessionParameters(m map[string]any) error

func (*WebhookResponse) WriteResponse

func (res *WebhookResponse) WriteResponse(w io.Writer) error

Directories

Path Synopsis
examples
gcp

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL