Skip to content
Permalink
Browse files

Initial lib commit

  • Loading branch information...
Depado committed Mar 14, 2018
1 parent 68013d6 commit 7c117036bc4cd17b78b6cf497b39acac2688ebab
Showing with 933 additions and 2 deletions.
  1. +19 −0 .drone.yml
  2. +2 −0 .gitignore
  3. +16 −2 README.md
  4. +13 −0 context.go
  5. +68 −0 dialogflow.go
  6. +149 −0 dialogflow_test.go
  7. +66 −0 fulfillment.go
  8. +95 −0 fulfillment_test.go
  9. +57 −0 helpers_test.go
  10. +34 −0 location.go
  11. +31 −0 location_test.go
  12. +17 −0 platform.go
  13. +198 −0 types.go
  14. +168 −0 types_test.go
@@ -0,0 +1,19 @@
workspace:
base: /go
path: src/github.com/leboncoin/dialogflow-go-webhook/

pipeline:

prerequisites:
image: golang:1.10
commands:
- go get ./...

test:
image: golang:1.10
commands:
- go test -race -coverprofile=coverage.txt -covermode=atomic

codecov:
image: robertstettner/drone-codecov
secrets: [ codecov_token ]
@@ -12,3 +12,5 @@

# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
.vscode
vendor/
@@ -1,2 +1,16 @@
# dialogflow-go-webhook
Simple package to create DialogFlow v2 webhooks using Go
# Dialogflow Go Webhook

![Go Version](https://img.shields.io/badge/go-1.9-brightgreen.svg)
![Go Version](https://img.shields.io/badge/go-1.10-brightgreen.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/leboncoin/dialogflow-go-webhook)](https://goreportcard.com/report/github.com/leboncoin/dialogflow-go-webhook)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/leboncoin/dialogflow-go-webhook/blob/master/LICENSE)
[![Godoc](https://godoc.org/github.com/leboncoin/dialogflow-go-webhook?status.svg)](https://godoc.org/github.com/leboncoin/dialogflow-go-webhook)


Simple library to create compatible DialogFlow v2 webhooks using Go.

This package is only intended to create webhooks, it doesn't implement the whole
DialogFlow API.

## Usage

@@ -0,0 +1,13 @@
package dialogflow

import "encoding/json"

// Context is a context contained in a query
type Context struct {
Name string `json:"name,omitempty"`
LifespanCount int `json:"lifespanCount,omitempty"`
Parameters json.RawMessage `json:"parameters,omitempty"`
}

// Contexts is a slice of pointer to Context
type Contexts []*Context
@@ -0,0 +1,68 @@
package dialogflow

import (
"encoding/json"
"errors"
"fmt"
"strings"
)

// Response is the top-level struct holding all the information
// Basically links a response ID with a query result.
type Response struct {
Session string `json:"session,omitempty"`
ResponseID string `json:"responseId,omitempty"`
QueryResult QueryResult `json:"queryResult,omitempty"`
OriginalDetectIntentRequest json.RawMessage `json:"originalDetectIntentRequest,omitempty"`
}

// GetParams simply unmarshals the parameters to the given struct and returns
// an error if it's not possible
func (rw *Response) GetParams(i interface{}) error {
return json.Unmarshal(rw.QueryResult.Parameters, &i)
}

// GetContext allows to search in the output contexts of the query
func (rw *Response) GetContext(ctx string, i interface{}) error {
for _, c := range rw.QueryResult.OutputContexts {
if strings.HasSuffix(c.Name, ctx) {
return json.Unmarshal(c.Parameters, &i)
}
}
return errors.New("context not found")
}

// NewContext is a helper function to create a new named context with params
// name and a lifespan
func (rw *Response) NewContext(name string, lifespan int, params interface{}) (*Context, error) {
var err error
var b []byte

if b, err = json.Marshal(params); err != nil {
return nil, err
}
ctx := &Context{
Name: fmt.Sprintf("%s/contexts/%s", rw.Session, name),
LifespanCount: lifespan,
Parameters: b,
}
return ctx, nil
}

// QueryResult is the dataset sent back by DialogFlow
type QueryResult struct {
QueryText string `json:"queryText,omitempty"`
Action string `json:"action,omitempty"`
LanguageCode string `json:"languageCode,omitempty"`
AllRequiredParamsPresent bool `json:"allRequiredParamsPresent,omitempty"`
IntentDetectionConfidence float64 `json:"intentDetectionConfidence,omitempty"`
Parameters json.RawMessage `json:"parameters,omitempty"`
OutputContexts []*Context `json:"outputContexts,omitempty"`
Intent Intent `json:"intent,omitempty"`
}

// Intent describes the matched intent
type Intent struct {
Name string `json:"name,omitempty"`
DisplayName string `json:"displayName,omitempty"`
}
@@ -0,0 +1,149 @@
package dialogflow

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

func TestResponse_GetParams(t *testing.T) {
type out struct {
In string `json:"in"`
Out string `json:"out"`
}
tests := []struct {
name string
params json.RawMessage
expected out
expectError bool
}{
{"should unmarshal fine", []byte(`{"in": "in", "out": "out"}`), out{"in", "out"}, false},
{"should be empty with other data", []byte(`{"hello": "world"}`), out{}, false},
{"should err", []byte(``), out{}, true},
{"should be empty", []byte(`{}`), out{}, false},
{"should match", []byte(`{"in": "helloworld", "out": "helloworld"}`), out{"helloworld", "helloworld"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rw := &Response{QueryResult: QueryResult{Parameters: tt.params}}

var output out
if err := rw.GetParams(&output); (err != nil) != tt.expectError {
t.Errorf("Response.GetParams() error = %v, wantErr %v", err, tt.expectError)
}
assert.Equal(t, output, tt.expected, "should match")
})
}
}

func TestResponse_GetContext(t *testing.T) {
type out struct {
In string `json:"in"`
Out string `json:"out"`
}
tests := []struct {
name string
fields Contexts
ctx string
expected out
wantErr bool
}{
{
"should find and unmarshal",
Contexts{{"hello-ctx", 1, []byte(`{"in": "in", "out": "out"}`)}},
"hello-ctx",
out{"in", "out"},
false,
},
{
"should fail",
Contexts{{"hello-ctx", 1, []byte(`{"in": "in", "out": "out"}`)}},
"random-ctx",
out{},
true,
},
{
"should work with multiple contexts",
Contexts{
{"random-ctx", 1, []byte(`{"in": "rand", "out": "rand"}`)},
{"hello-ctx", 1, []byte(`{"in": "in", "out": "out"}`)},
},
"hello-ctx",
out{"in", "out"},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rw := &Response{QueryResult: QueryResult{OutputContexts: tt.fields}}

var output out
if err := rw.GetContext(tt.ctx, &output); (err != nil) != tt.wantErr {
t.Errorf("Response.GetContext() error = %v, wantErr %v", err, tt.wantErr)
}
assert.Equal(t, output, tt.expected, "should match")
})
}
}

func TestResponse_NewContext(t *testing.T) {
type out struct {
In string `json:"in"`
Out string `json:"out"`
}
type fields struct {
Session string
ResponseID string
QueryResult QueryResult
OriginalDetectIntentRequest json.RawMessage
}
std := fields{Session: "session"}
type args struct {
name string
lifespan int
params interface{}
}
tests := []struct {
name string
fields fields
args args
want *Context
wantErr bool
}{
{
"should generate properly",
std,
args{"hello-ctx", 3, out{"hello", "world"}},
&Context{"session/contexts/hello-ctx", 3, []byte(`{"in": "hello", "out": "world"}`)},
false,
},
{
"should error",
std,
args{"hello-ctx", 3, make(chan int)},
&Context{},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rw := &Response{
Session: tt.fields.Session,
ResponseID: tt.fields.ResponseID,
QueryResult: tt.fields.QueryResult,
OriginalDetectIntentRequest: tt.fields.OriginalDetectIntentRequest,
}
got, err := rw.NewContext(tt.args.name, tt.args.lifespan, tt.args.params)
if (err != nil) != tt.wantErr {
t.Errorf("Response.NewContext() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
assert.Equal(t, got.Name, tt.want.Name)
assert.Equal(t, got.LifespanCount, tt.want.LifespanCount)
})
}
}
@@ -0,0 +1,66 @@
package dialogflow

import (
"bytes"
"encoding/json"
"fmt"
)

// Fulfillment is the response sent back to dialogflow in case of a successful
// webhook call
type Fulfillment struct {
FulfillmentText string `json:"fulfillmentText,omitempty"`
FulfillmentMessages Messages `json:"fulfillmentMessages,omitempty"`
Source string `json:"source,omitempty"`
Payload interface{} `json:"payload,omitempty"`
OutputContexts Contexts `json:"outputContexts,omitempty"`
FollowupEventInput interface{} `json:"followupEventInput,omitempty"`
}

// Messages is a simple slice of Message
type Messages []Message

// RichMessage is an interface used in the Message type.
// It is used to send back payloads to dialogflow
type RichMessage interface {
GetKey() string
}

// Message is a struct holding a platform and a RichMessage.
// Used in the FulfillmentMessages of the response sent back to dialogflow
type Message struct {
Platform
RichMessage RichMessage
}

// MarshalJSON implements the Marshaller interface for the JSON type.
// Custom marshalling is necessary since there can only be one rich message
// per Message and the key associated to each type is dynamic
func (m *Message) MarshalJSON() ([]byte, error) {
var err error
var b []byte
buffer := bytes.NewBufferString("{")
if m.Platform != "" {
buffer.WriteString(fmt.Sprintf(`"platform": "%s"`, m.Platform))
}
if m.Platform != "" && m.RichMessage != nil {
buffer.WriteString(", ")
}
if m.RichMessage != nil {
if b, err = json.Marshal(m.RichMessage); err != nil {
return nil, err
}
buffer.WriteString(fmt.Sprintf(`"%s": %s`, m.RichMessage.GetKey(), string(b)))
}
buffer.WriteString("}")
return buffer.Bytes(), nil
}

// ForGoogle takes a rich message wraps it in a message with the appropriate
// platform set
func ForGoogle(r RichMessage) Message {
return Message{
Platform: ActionsOnGoogle,
RichMessage: r,
}
}

0 comments on commit 7c11703

Please sign in to comment.
You can’t perform that action at this time.