Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.envrc
cover.out
ngrok.yml
cover.out

*.mp3
*.ogg
*.opus
*.wav
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,31 @@

Package alice provides helpers for developing skills for Alice virtual assistant
via Yandex.Dialogs platform.

# Example

```go
responder := func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) {
return &alice.ResponsePayload{
Text: "Bye!",
EndSession: true,
}, nil
}

h := alice.NewHandler(responder)
h.Errorf = log.Printf
http.Handle("/", h)

const addr = "127.0.0.1:8080"
log.Printf("Listening on http://%s ...", addr)
log.Fatal(http.ListenAndServe(addr, nil))
```

See [documentation](https://godoc.org/github.com/AlekSi/alice) and [examples](examples).

# License

Copyright (c) 2019 Alexey Palazhchenko. [MIT-style license](LICENSE).

Tests use the following resources:
* https://freesound.org/people/prucanada/sounds/415341/ by prucanada.
5 changes: 3 additions & 2 deletions doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import (
)

func Example() {
h := alice.NewHandler(func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) {
responder := func(ctx context.Context, request *alice.Request) (*alice.ResponsePayload, error) {
return &alice.ResponsePayload{
Text: "Bye!",
EndSession: true,
}, nil
})
}

h := alice.NewHandler(responder)
h.Errorf = log.Printf
http.Handle("/", h)

Expand Down
2 changes: 2 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
req.Body = ioutil.NopCloser(&body)
req.ContentLength = int64(body.Len())
req.TransferEncoding = nil
}

b, err := httputil.DumpRequest(req, true)
Expand Down
204 changes: 204 additions & 0 deletions resources/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package resources
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ST1000: at least one file in a package should have a package comment (from stylecheck)


import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httputil"
"os"
"path/filepath"
"time"

"github.com/AlekSi/alice"
)

type Quota struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported type Quota should have comment or be unexported (from golint)

Total int
Used int
}

type Sound struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported type Sound should have comment or be unexported (from golint)

ID string
SkillID string
Size *int
OriginalName string
CreatedAt time.Time
IsProcessed bool
Error *string
}

type Client struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported type Client should have comment or be unexported (from golint)

SkillID string
OAuthToken string
HTTPClient *http.Client

// debugging options
Debugf alice.Printf // debug logger
Indent bool // indent requests and responses
StrictDecoder bool // disallow unexpected fields in responses
}

func (c *Client) do(req *http.Request, respBody interface{}) error {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cognitive complexity 32 of func (*Client).do is high (> 30) (from gocognit)

httpClient := c.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}

var jsonRequst bool
if c.OAuthToken != "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if statements should only be cuddled with assignments used in the if statement itself (from wsl)

req.Header.Set("Authorization", "OAuth "+c.OAuthToken)
}
if req.Body != nil && req.Header.Get("Content-Type") == "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if statements should only be cuddled with assignments (from wsl)

jsonRequst = true
req.Header.Set("Content-Type", "application/json; charset=utf-8")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only cuddled expressions if assigning variable or using from line above (from wsl)

}

if c.Debugf != nil {
if c.Indent && jsonRequst {
b, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}

var body bytes.Buffer
if err = json.Indent(&body, b, "", " "); err != nil {
return err
}
req.Body = ioutil.NopCloser(&body)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assignments should only be cuddled with other assignments (from wsl)

req.ContentLength = int64(body.Len())
req.TransferEncoding = nil
}

b, err := httputil.DumpRequestOut(req, jsonRequst)
if err != nil {
return err
}
c.debugf("Request:\n%s", b)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expressions should not be cuddled with blocks (from wsl)

}

resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close() //nolint:errcheck

if c.Debugf != nil {
if c.Indent {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

var body bytes.Buffer
if err = json.Indent(&body, b, "", " "); err != nil {
return err
}
resp.Body = ioutil.NopCloser(&body)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assignments should only be cuddled with other assignments (from wsl)

resp.ContentLength = int64(body.Len())
resp.TransferEncoding = nil
}

b, err := httputil.DumpResponse(resp, true)
if err != nil {
return err
}
c.debugf("Response:\n%s", b)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expressions should not be cuddled with blocks (from wsl)

}

if resp.StatusCode/100 != 2 {
return fmt.Errorf("status code %d", resp.StatusCode)
}

d := json.NewDecoder(resp.Body)
if c.StrictDecoder {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if statements should only be cuddled with assignments used in the if statement itself (from wsl)

d.DisallowUnknownFields()
}
return d.Decode(&respBody)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return statements should not be cuddled if block has more than two lines (from wsl)

}

func (c *Client) debugf(format string, a ...interface{}) {
if c.Debugf != nil {
c.Debugf(format, a...)
}
}

type StatusResponse struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported type StatusResponse should have comment or be unexported (from golint)

Images struct {
Quota Quota
}
Sounds struct {
Quota Quota
}
}

func (c *Client) Status() (*StatusResponse, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported method Client.Status should have comment or be unexported (from golint)

req, err := http.NewRequest("GET", "https://dialogs.yandex.net/api/v1/status", nil)
if err != nil {
return nil, err
}

var res StatusResponse
if err = c.do(req, &res); err != nil {
return nil, err
}
return &res, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return statements should not be cuddled if block has more than two lines (from wsl)

}

func (c *Client) UploadSound(name string, r io.Reader) (*Sound, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported method Client.UploadSound should have comment or be unexported (from golint)

var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
fw, err := mw.CreateFormFile("file", name)
if err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one cuddle assignment allowed before if statement (from wsl)

return nil, err
}
if _, err = io.Copy(fw, r); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if statements should only be cuddled with assignments (from wsl)

return nil, err
}
if err = mw.Close(); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if statements should only be cuddled with assignments (from wsl)

return nil, err
}

req, err := http.NewRequest("POST", "https://dialogs.yandex.net/api/v1/skills/"+c.SkillID+"/sounds", &buf)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", mw.FormDataContentType())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expressions should not be cuddled with blocks (from wsl)


var res struct {
Sound Sound
}
if err = c.do(req, &res); err != nil {
return nil, err
}
return &res.Sound, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return statements should not be cuddled if block has more than two lines (from wsl)

}

func (c *Client) UploadSoundFile(filename string) (*Sound, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported method Client.UploadSoundFile should have comment or be unexported (from golint)

f, err := os.Open(filename)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

G304: Potential file inclusion via variable (from gosec)

if err != nil {
return nil, err
}
defer f.Close() //nolint:errcheck

return c.UploadSound(filepath.Base(filename), f)
}

func (c *Client) ListSounds() ([]Sound, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported method Client.ListSounds should have comment or be unexported (from golint)

req, err := http.NewRequest("GET", "https://dialogs.yandex.net/api/v1/skills/"+c.SkillID+"/sounds", nil)
if err != nil {
return nil, err
}

var res struct {
Sounds []Sound
Total int
}
if err = c.do(req, &res); err != nil {
return nil, err
}
return res.Sounds, nil
}
60 changes: 60 additions & 0 deletions resources/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package resources

import (
"os"
"path/filepath"
"testing"
"time"

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

func TestClient(t *testing.T) {
if testing.Short() {
t.Skip("-short is passed, skipping integration test")
}

skillID := os.Getenv("ALICE_TEST_SKILL_ID")
oAuthToken := os.Getenv("ALICE_TEST_OAUTH_TOKEN")
if skillID == "" || oAuthToken == "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one cuddle assignment allowed before if statement (from wsl)

t.Skip("`ALICE_TEST_SKILL_ID` or `ALICE_TEST_OAUTH_TOKEN` is not set, skipping integration test")
}

c := Client{
SkillID: skillID,
OAuthToken: oAuthToken,
Indent: true,
StrictDecoder: true,
}

t.Run("Status", func(t *testing.T) {
c.Debugf = t.Logf

status, err := c.Status()
require.NoError(t, err)
assert.Equal(t, 104857600, status.Images.Quota.Total)
assert.Equal(t, 1073741824, status.Sounds.Quota.Total)
})

t.Run("Sound", func(t *testing.T) {
t.Run("UploadSoundFile", func(t *testing.T) {
c.Debugf = t.Logf

sound, err := c.UploadSoundFile(filepath.Join("..", "testdata", "go.wav"))
require.NoError(t, err)
require.NotEmpty(t, sound)
assert.NotEmpty(t, skillID, sound.ID)
assert.Equal(t, skillID, sound.SkillID)
assert.Empty(t, sound.Size)
assert.Equal(t, "go.wav", sound.OriginalName)
assert.WithinDuration(t, time.Now(), sound.CreatedAt, 5*time.Second)
assert.False(t, sound.IsProcessed)
assert.Nil(t, sound.Error)

sounds, err := c.ListSounds()
require.NoError(t, err)
assert.Empty(t, sounds)
})
})
}