Switch to dep from glide (#664)

This commit is contained in:
Travis Reeder
2018-01-09 14:11:08 -08:00
committed by Reed Allman
parent 0a09d74137
commit 3b9818bc58
1074 changed files with 10019 additions and 107660 deletions

View File

@@ -53,9 +53,9 @@ asking for suggestions on how to address the documentation part.
### Build
Use go 1.9.1 or newer.
Requires Go >= 1.9.1.
The first time after you fork or after dependencies get updated, run:
The first time after you clone or after dependencies get updated, run:
```sh
make dep

604
Gopkg.lock generated Normal file
View File

@@ -0,0 +1,604 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/Azure/go-ansiterm"
packages = [".","winterm"]
revision = "19f72df4d05d31cbe1c56bfc8045c96babff6c7e"
[[projects]]
name = "github.com/Microsoft/go-winio"
packages = ["."]
revision = "78439966b38d69bf38227fbf57ac8a6fee70f69a"
version = "v0.4.5"
[[projects]]
branch = "master"
name = "github.com/Nvveen/Gotty"
packages = ["."]
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
[[projects]]
name = "github.com/PuerkitoBio/purell"
packages = ["."]
revision = "8a290539e2e8629dbc4e6bad948158f790ec31f4"
version = "v1.0.0"
[[projects]]
name = "github.com/PuerkitoBio/urlesc"
packages = ["."]
revision = "5bd2802263f21d8788851d5305584c82a5c75d7e"
[[projects]]
name = "github.com/Shopify/sarama"
packages = ["."]
revision = "4704a3a8c95920361c47e9a2adec13c3d757c757"
[[projects]]
name = "github.com/apache/thrift"
packages = ["lib/go/thrift"]
revision = "4c30c15924bfbc7c9e6bfc0e82630e97980e556e"
[[projects]]
name = "github.com/asaskevich/govalidator"
packages = ["."]
revision = "15028e809df8c71964e8efa6c11e81d5c0262302"
[[projects]]
branch = "master"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
[[projects]]
name = "github.com/boltdb/bolt"
packages = ["."]
revision = "fa5367d20c994db73282594be0146ab221657943"
[[projects]]
name = "github.com/cloudflare/cfssl"
packages = ["api","auth","certdb","config","crypto/pkcs7","csr","errors","helpers","helpers/derhelpers","info","initca","log","ocsp/config","signer","signer/local"]
revision = "7d88da830aad9d533c2fb8532da23f6a75331b52"
[[projects]]
name = "github.com/coreos/etcd"
packages = ["raft/raftpb"]
revision = "5bb9f9591f01d0a3c61d2eb3a3bb281726005b2b"
[[projects]]
name = "github.com/coreos/go-semver"
packages = ["semver"]
revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6"
version = "v0.2.0"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
[[projects]]
branch = "master"
name = "github.com/dchest/siphash"
packages = ["."]
revision = "4ebf1de738443ea7f45f02dc394c4df1942a126d"
[[projects]]
name = "github.com/docker/distribution"
packages = [".","digestset","manifest","manifest/schema1","manifest/schema2","reference","registry/api/errcode","registry/api/v2","registry/client","registry/client/auth","registry/client/auth/challenge","registry/client/transport","registry/storage/cache","registry/storage/cache/memory"]
revision = "bc3c7b0525e59d3ecfab3e1568350895fd4a462f"
[[projects]]
name = "github.com/docker/docker"
packages = ["api/types","api/types/blkiodev","api/types/container","api/types/filters","api/types/mount","api/types/network","api/types/registry","api/types/strslice","api/types/swarm","api/types/swarm/runtime","api/types/versions","daemon/cluster/convert","opts","pkg/archive","pkg/fileutils","pkg/homedir","pkg/idtools","pkg/ioutils","pkg/jsonlog","pkg/jsonmessage","pkg/longpath","pkg/mount","pkg/namesgenerator","pkg/pools","pkg/promise","pkg/stdcopy","pkg/system","pkg/term","pkg/term/windows"]
revision = "cdf870bd0b5fa678b10ef2708cca7ad776b4913c"
[[projects]]
name = "github.com/docker/go-connections"
packages = ["nat"]
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "github.com/docker/go-events"
packages = ["."]
revision = "9461782956ad83b30282bf90e31fa6a70c255ba9"
[[projects]]
name = "github.com/docker/go-units"
packages = ["."]
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
version = "v0.3.2"
[[projects]]
name = "github.com/docker/libkv"
packages = [".","store"]
revision = "93ab0e6c056d325dfbb11e1d58a3b4f5f62e7f3c"
[[projects]]
name = "github.com/docker/libnetwork"
packages = ["datastore","discoverapi","types"]
revision = "6d098467ec58038b68620a3c2c418936661efa64"
[[projects]]
branch = "master"
name = "github.com/docker/libtrust"
packages = ["."]
revision = "aabc10ec26b754e797f9028f4589c5b7bd90dc20"
[[projects]]
name = "github.com/docker/swarmkit"
packages = ["api","api/deepcopy","api/equality","api/genericresource","api/naming","ca","connectionbroker","identity","ioutils","log","manager/raftselector","manager/state","manager/state/store","protobuf/plugin","remotes","watch","watch/queue"]
revision = "bd7bafb8a61de1f5f23c8215ce7b9ecbcb30ff21"
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
[[projects]]
name = "github.com/eapache/go-resiliency"
packages = ["breaker"]
revision = "b1fe83b5b03f624450823b751b662259ffc6af70"
[[projects]]
branch = "master"
name = "github.com/eapache/go-xerial-snappy"
packages = ["."]
revision = "bb955e01b9346ac19dc29eb16586c90ded99a98c"
[[projects]]
name = "github.com/eapache/queue"
packages = ["."]
revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98"
version = "v1.1.0"
[[projects]]
name = "github.com/emicklei/go-restful"
packages = [".","log"]
revision = "ff4f55a206334ef123e4f79bbf348980da81ca46"
[[projects]]
name = "github.com/emicklei/go-restful-swagger12"
packages = ["."]
revision = "dcef7f55730566d41eae5db10e7d6981829720f6"
version = "1.0.1"
[[projects]]
branch = "master"
name = "github.com/fnproject/fdk-go"
packages = ["."]
revision = "7c1e1a329cf1004edf545318280d672f35e15081"
[[projects]]
name = "github.com/fnproject/fn_go"
packages = ["client","client/apps","client/call","client/operations","client/routes","models"]
revision = "7ce3bb2e624df60cdfbfc1ee5483f6df80bb2b1b"
version = "0.2.1"
[[projects]]
name = "github.com/fsouza/go-dockerclient"
packages = ["."]
revision = "98edf3edfae6a6500fecc69d2bcccf1302544004"
[[projects]]
name = "github.com/garyburd/redigo"
packages = ["internal","redis"]
revision = "70e1b1943d4fc9c56791abaa6f4d1e727b9ab925"
[[projects]]
name = "github.com/ghodss/yaml"
packages = ["."]
revision = "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
[[projects]]
name = "github.com/gin-contrib/cors"
packages = ["."]
revision = "cf4846e6a636a76237a28d9286f163c132e841bc"
version = "v1.2"
[[projects]]
branch = "master"
name = "github.com/gin-contrib/sse"
packages = ["."]
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
[[projects]]
name = "github.com/gin-gonic/gin"
packages = [".","binding","json","render"]
revision = "5afc5b19730118c9b8324fe9dd995d44ec65c81a"
[[projects]]
name = "github.com/go-ini/ini"
packages = ["."]
revision = "c787282c39ac1fc618827141a1f762240def08a3"
[[projects]]
name = "github.com/go-logfmt/logfmt"
packages = ["."]
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
version = "v0.3.0"
[[projects]]
name = "github.com/go-openapi/analysis"
packages = ["."]
revision = "8ed83f2ea9f00f945516462951a288eaa68bf0d6"
[[projects]]
name = "github.com/go-openapi/errors"
packages = ["."]
revision = "03cfca65330da08a5a440053faf994a3c682b5bf"
[[projects]]
branch = "master"
name = "github.com/go-openapi/jsonpointer"
packages = ["."]
revision = "779f45308c19820f1a69e9a4cd965f496e0da10f"
[[projects]]
name = "github.com/go-openapi/jsonreference"
packages = ["."]
revision = "13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272"
[[projects]]
name = "github.com/go-openapi/loads"
packages = ["."]
revision = "a80dea3052f00e5f032e860dd7355cd0cc67e24d"
[[projects]]
name = "github.com/go-openapi/runtime"
packages = [".","client"]
revision = "d6605b7c17ac3b1033ca794886e6142a4141f5b0"
[[projects]]
name = "github.com/go-openapi/spec"
packages = ["."]
revision = "3faa0055dbbf2110abc1f3b4e3adbb22721e96e7"
[[projects]]
branch = "master"
name = "github.com/go-openapi/strfmt"
packages = ["."]
revision = "4dd3d302e100bae008baedc42d446ce83bdd10ad"
[[projects]]
name = "github.com/go-openapi/swag"
packages = ["."]
revision = "f3f9494671f93fcff853e3c6e9e948b3eb71e590"
[[projects]]
name = "github.com/go-openapi/validate"
packages = ["."]
revision = "8a82927c942c94794a5cd8b8b50ce2f48a955c0c"
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "21d7e97c9f760ca685a01ecea202e1c84276daa1"
[[projects]]
name = "github.com/gogo/protobuf"
packages = ["gogoproto","proto","protoc-gen-gogo/descriptor","sortkeys","types"]
revision = "c0656edd0d9eab7c66d1eb0c568f9039345796f7"
[[projects]]
name = "github.com/golang/glog"
packages = ["."]
revision = "44145f04b68cf362d9c4df2182967c2275eaefed"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto","ptypes/any"]
revision = "4bd1920723d7b7c925de087aa32e2187708897f7"
[[projects]]
branch = "master"
name = "github.com/golang/snappy"
packages = ["."]
revision = "553a641470496b2327abcac10b36396bd98e45c9"
[[projects]]
branch = "master"
name = "github.com/google/btree"
packages = ["."]
revision = "316fb6d3f031ae8f4d457c6c5186b9e3ded70435"
[[projects]]
name = "github.com/google/certificate-transparency-go"
packages = [".","asn1","client","jsonclient","tls","x509","x509/pkix"]
revision = "0dac42a6ed448ba220ee315abfaa6d26fd5fc9bb"
[[projects]]
name = "github.com/google/gofuzz"
packages = ["."]
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
[[projects]]
branch = "master"
name = "github.com/gorilla/context"
packages = ["."]
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "24fca303ac6da784b9e8269f724ddeb0b2eea5e7"
version = "v1.5.0"
[[projects]]
name = "github.com/grpc-ecosystem/go-grpc-prometheus"
packages = ["."]
revision = "6b7015e65d366bf3f19b2b2a000a831940f0f7e0"
version = "v1.1"
[[projects]]
branch = "master"
name = "github.com/hashicorp/go-immutable-radix"
packages = ["."]
revision = "8aac2701530899b64bdea735a1de8da899815220"
[[projects]]
name = "github.com/hashicorp/go-memdb"
packages = ["."]
revision = "ec43fcf8f202880feb35d2abb40a570c1f4172e9"
[[projects]]
name = "github.com/hashicorp/golang-lru"
packages = ["simplelru"]
revision = "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
[[projects]]
name = "github.com/jmoiron/sqlx"
packages = [".","reflectx"]
revision = "d9bd385d68c068f1fabb5057e3dedcbcbb039d0f"
[[projects]]
name = "github.com/json-iterator/go"
packages = ["."]
revision = "fdfe0b9a69118ff692d6e1005e9de7e0cffb7d6b"
[[projects]]
name = "github.com/juju/ratelimit"
packages = ["."]
revision = "5b9ff866471762aa2ab2dced63c9fb6f53921342"
[[projects]]
branch = "master"
name = "github.com/kr/logfmt"
packages = ["."]
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
[[projects]]
name = "github.com/lib/pq"
packages = [".","oid"]
revision = "23da1db4f16d9658a86ae9b717c245fc078f10f1"
[[projects]]
branch = "master"
name = "github.com/mailru/easyjson"
packages = ["buffer","jlexer","jwriter"]
revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1"
[[projects]]
name = "github.com/mattes/migrate"
packages = [".","database","source"]
revision = "5b98c13eff7657ab49a1a5f705b72f961d7fc558"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
version = "v0.0.2"
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
revision = "05548ff55570cdb9ac72ff4a25a3b5e77a6fb7e5"
[[projects]]
branch = "master"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
[[projects]]
branch = "master"
name = "github.com/minio/go-homedir"
packages = ["."]
revision = "4d76aabb80b22bad8695d3904e943f1fb5e6199f"
[[projects]]
name = "github.com/minio/minio-go"
packages = [".","pkg/credentials","pkg/encrypt","pkg/policy","pkg/s3signer","pkg/s3utils","pkg/set"]
revision = "a62e2045a5d3a6630dbb7040260994583ac56b10"
version = "4.0.1"
[[projects]]
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
[[projects]]
name = "github.com/opencontainers/go-digest"
packages = ["."]
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
version = "v1.0.0-rc1"
[[projects]]
name = "github.com/opencontainers/image-spec"
packages = ["specs-go","specs-go/v1"]
revision = "ebd93fd0782379ca3d821f0fa74f0651a9347a3e"
[[projects]]
name = "github.com/opencontainers/runc"
packages = ["libcontainer/system","libcontainer/user"]
revision = "ae2948042b08ad3d6d13cd09f40a50ffff4fc688"
[[projects]]
branch = "master"
name = "github.com/opentracing-contrib/go-observer"
packages = ["."]
revision = "a52f2342449246d5bcc273e65cbdcfa5f7d6c63c"
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [".","ext","log"]
revision = "8ebe5d4e236eed9fd88e593c288bfb804d630b8c"
[[projects]]
name = "github.com/openzipkin/zipkin-go-opentracing"
packages = [".","flag","thrift/gen-go/scribe","thrift/gen-go/zipkincore","types","wire"]
revision = "9c88fa03bfdfaa5fec7cd1b40f3d10ec15c15fc6"
version = "v0.3.1"
[[projects]]
name = "github.com/patrickmn/go-cache"
packages = ["."]
revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
version = "v2.1.0"
[[projects]]
branch = "master"
name = "github.com/petar/GoLLRB"
packages = ["llrb"]
revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
[[projects]]
name = "github.com/pierrec/lz4"
packages = ["."]
revision = "08c27939df1bd95e881e2c2367a749964ad1fceb"
version = "v1.0.1"
[[projects]]
branch = "master"
name = "github.com/pierrec/xxHash"
packages = ["xxHash32"]
revision = "a0006b13c722f7f12368c00a3d3c2ae8a999a0c6"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "2b3a18b5f0fb6b4f9190549597d3f962c02bc5eb"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = ["prometheus","prometheus/promhttp"]
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "6f3806018612930941127f2a7c6c453ba2c527d2"
[[projects]]
name = "github.com/prometheus/common"
packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"]
revision = "2f17f4a9d485bf34b4bfaccc273805040e4f86c8"
[[projects]]
name = "github.com/prometheus/procfs"
packages = [".","xfs"]
revision = "a1dba9ce8baed984a2495b658c82687f8157b98f"
[[projects]]
name = "github.com/rcrowley/go-metrics"
packages = ["."]
revision = "1f30fe9094a513ce4c700b9a54458bbb0c96996c"
[[projects]]
name = "github.com/rdallman/migrate"
packages = [".","database/mysql","database/postgres","database/sqlite3","source","source/go-bindata"]
revision = "bc72eeb997c7334cb5f05f5aefd2d70bc34d71ef"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = [".","hooks/syslog"]
revision = "89742aefa4b206dcf400792f3bd35b542998eb3b"
[[projects]]
name = "github.com/spf13/pflag"
packages = ["."]
revision = "9ff6c6923cfffbcd502984b8e0c80539a94968b7"
[[projects]]
name = "github.com/ugorji/go"
packages = ["codec"]
revision = "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ocsp","pkcs12","pkcs12/internal/rc2","ssh/terminal"]
revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","context/ctxhttp","http2","http2/hpack","idna","internal/timeseries","lex/httplex","trace"]
revision = "a8b9294777976932365dabb6640cf1468d95c70f"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix","windows"]
revision = "8b4580aae2a0dd0c231a45d3ccb8434ff533b840"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = ["cases","collate","collate/build","internal","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","runes","secure/bidirule","secure/precis","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable","width"]
revision = "57961680700a5336d15015c8c50686ca5ba362a4"
[[projects]]
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
revision = "09f6ed296fc66555a25fe4ce95173148778dfa85"
[[projects]]
name = "google.golang.org/grpc"
packages = [".","codes","credentials","grpclb/grpc_lb_v1","grpclog","internal","keepalive","metadata","naming","peer","stats","status","tap","transport"]
revision = "b8669c35455183da6d5c474ea6e72fbf55183274"
version = "v1.5.1"
[[projects]]
name = "gopkg.in/go-playground/validator.v8"
packages = ["."]
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
version = "v8.18.2"
[[projects]]
name = "gopkg.in/inf.v0"
packages = ["."]
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
version = "v0.9.0"
[[projects]]
branch = "v2"
name = "gopkg.in/mgo.v2"
packages = ["bson","internal/json"]
revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "53feefa2559fb8dfa8d81baad31be332c97d6c77"
[[projects]]
name = "k8s.io/apimachinery"
packages = ["pkg/api/equality","pkg/api/errors","pkg/api/meta","pkg/api/resource","pkg/apimachinery","pkg/apimachinery/announced","pkg/apimachinery/registered","pkg/apis/meta/v1","pkg/apis/meta/v1/unstructured","pkg/apis/meta/v1alpha1","pkg/conversion","pkg/conversion/queryparams","pkg/conversion/unstructured","pkg/fields","pkg/labels","pkg/openapi","pkg/runtime","pkg/runtime/schema","pkg/runtime/serializer","pkg/runtime/serializer/json","pkg/runtime/serializer/protobuf","pkg/runtime/serializer/recognizer","pkg/runtime/serializer/streaming","pkg/runtime/serializer/versioning","pkg/selection","pkg/types","pkg/util/clock","pkg/util/diff","pkg/util/errors","pkg/util/framer","pkg/util/intstr","pkg/util/json","pkg/util/net","pkg/util/rand","pkg/util/runtime","pkg/util/sets","pkg/util/validation","pkg/util/validation/field","pkg/util/wait","pkg/util/yaml","pkg/version","pkg/watch","third_party/forked/golang/reflect"]
revision = "1fd2e63a9a370677308a42f24fd40c86438afddf"
[[projects]]
name = "k8s.io/client-go"
packages = ["discovery","kubernetes","kubernetes/scheme","kubernetes/typed/admissionregistration/v1alpha1","kubernetes/typed/apps/v1beta1","kubernetes/typed/authentication/v1","kubernetes/typed/authentication/v1beta1","kubernetes/typed/authorization/v1","kubernetes/typed/authorization/v1beta1","kubernetes/typed/autoscaling/v1","kubernetes/typed/autoscaling/v2alpha1","kubernetes/typed/batch/v1","kubernetes/typed/batch/v2alpha1","kubernetes/typed/certificates/v1beta1","kubernetes/typed/core/v1","kubernetes/typed/extensions/v1beta1","kubernetes/typed/networking/v1","kubernetes/typed/policy/v1beta1","kubernetes/typed/rbac/v1alpha1","kubernetes/typed/rbac/v1beta1","kubernetes/typed/settings/v1alpha1","kubernetes/typed/storage/v1","kubernetes/typed/storage/v1beta1","pkg/api","pkg/api/v1","pkg/api/v1/ref","pkg/apis/admissionregistration","pkg/apis/admissionregistration/v1alpha1","pkg/apis/apps","pkg/apis/apps/v1beta1","pkg/apis/authentication","pkg/apis/authentication/v1","pkg/apis/authentication/v1beta1","pkg/apis/authorization","pkg/apis/authorization/v1","pkg/apis/authorization/v1beta1","pkg/apis/autoscaling","pkg/apis/autoscaling/v1","pkg/apis/autoscaling/v2alpha1","pkg/apis/batch","pkg/apis/batch/v1","pkg/apis/batch/v2alpha1","pkg/apis/certificates","pkg/apis/certificates/v1beta1","pkg/apis/extensions","pkg/apis/extensions/v1beta1","pkg/apis/networking","pkg/apis/networking/v1","pkg/apis/policy","pkg/apis/policy/v1beta1","pkg/apis/rbac","pkg/apis/rbac/v1alpha1","pkg/apis/rbac/v1beta1","pkg/apis/settings","pkg/apis/settings/v1alpha1","pkg/apis/storage","pkg/apis/storage/v1","pkg/apis/storage/v1beta1","pkg/util","pkg/util/parsers","pkg/version","rest","rest/watch","tools/clientcmd/api","tools/metrics","transport","util/cert","util/flowcontrol","util/integer"]
revision = "d92e8497f71b7b4e0494e5bd204b48d34bd6f254"
version = "v4.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "c0ba99cc154500d7d551e61f56d172037c20d1e3e0b119c486e9aae15680b586"
solver-name = "gps-cdcl"
solver-version = 1

106
Gopkg.toml Normal file
View File

@@ -0,0 +1,106 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
ignored = ["github.com/fnproject/fn/cli"]
[[constraint]]
name = "github.com/boltdb/bolt"
revision = "fa5367d20c994db73282594be0146ab221657943"
[[constraint]]
name = "github.com/coreos/go-semver"
version = "^0.2.0"
[[constraint]]
branch = "master"
name = "github.com/dchest/siphash"
[[constraint]]
name = "github.com/fnproject/fn_go"
version = "0.2.0"
[[constraint]]
name = "github.com/gin-contrib/cors"
version = "~1.2.0"
[[constraint]]
branch = "master"
name = "github.com/go-openapi/strfmt"
[[constraint]]
branch = "master"
name = "github.com/google/btree"
[[constraint]]
name = "github.com/minio/minio-go"
version = "4.0.1"
[[constraint]]
name = "github.com/openzipkin/zipkin-go-opentracing"
version = "0.3.1"
[[constraint]]
name = "github.com/patrickmn/go-cache"
version = "2.1.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
[[constraint]]
name = "github.com/sirupsen/logrus"
revision = "89742aefa4b206dcf400792f3bd35b542998eb3b"
[[constraint]]
name = "github.com/docker/docker"
revision = "cdf870bd0b5fa678b10ef2708cca7ad776b4913c"
[[constraint]]
name = "github.com/docker/cli"
revision = "139fcd3ee95f37f3ac17b1200fb0a63908cb6781"
[[constraint]]
name = "github.com/docker/distribution"
revision = "bc3c7b0525e59d3ecfab3e1568350895fd4a462f"
[[constraint]]
name = "github.com/rdallman/migrate" # TODO change to mattes/migrate w/ https://github.com/mattes/migrate/pull/299
revision = "bc72eeb997c7334cb5f05f5aefd2d70bc34d71ef"
[[constraint]]
name = "github.com/go-sql-driver/mysql"
revision = "21d7e97c9f760ca685a01ecea202e1c84276daa1"
[[constraint]]
name = "github.com/opencontainers/go-digest"
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
[[constraint]]
name = "github.com/opencontainers/runc"
revision = "ae2948042b08ad3d6d13cd09f40a50ffff4fc688"
[[constraint]]
name = "github.com/Azure/go-ansiterm"
revision = "19f72df4d05d31cbe1c56bfc8045c96babff6c7e"
[[constraint]]
name = "github.com/prometheus/common"
revision = "2f17f4a9d485bf34b4bfaccc273805040e4f86c8"

View File

@@ -2,10 +2,10 @@
.PHONY: all test dep build test-log-datastore checkfmt pull-images api-test fn-test-utils test-middleware test-extensions test-basic test-api
dep:
glide install -v
dep ensure --vendor-only
dep-up:
glide up -v
dep ensure
build:
go build -o fnserver
@@ -70,10 +70,6 @@ test-build-arm:
run: build
GIN_MODE=debug ./fnserver
docker-dep:
# todo: need to create a dep tool image for this (or just ditch this)
docker run --rm -it -v ${CURDIR}:/go/src/github.com/fnproject/fn -w /go/src/github.com/fnproject/fn treeder/glide install -v
docker-build:
docker build --build-arg HTTPS_PROXY --build-arg HTTP_PROXY -t fnproject/fnserver:latest .

View File

@@ -90,7 +90,7 @@ func CheckRegistry(ctx context.Context, image string, config docker.AuthConfigur
tran = &retryWrap{cm, tran}
repo, err := registry.NewRepository(ctx, repoNamed, regURL, tran)
repo, err := registry.NewRepository(repoNamed, regURL, tran)
if err != nil {
return nil, err
}

595
glide.lock generated
View File

@@ -1,595 +0,0 @@
hash: 3fa5b86f68121ba1422761f2937e6a9b7f5c330adc6f84e2f3b7b78b87a747d7
updated: 2017-12-04T17:26:34.506649Z
imports:
- name: github.com/amir/raidman
version: 1ccc43bfb9c93cb401a4025e49c64ba71e5e668b
- name: github.com/apache/thrift
version: 4c30c15924bfbc7c9e6bfc0e82630e97980e556e
subpackages:
- lib/go/thrift
- name: github.com/asaskevich/govalidator
version: 15028e809df8c71964e8efa6c11e81d5c0262302
- name: github.com/Azure/go-ansiterm
version: 19f72df4d05d31cbe1c56bfc8045c96babff6c7e
subpackages:
- winterm
- name: github.com/beorn7/perks
version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9
subpackages:
- quantile
- name: github.com/boltdb/bolt
version: fa5367d20c994db73282594be0146ab221657943
- name: github.com/cactus/go-statsd-client
version: ce77ca9ecdee1c3ffd097e32f9bb832825ccb203
subpackages:
- statsd
- name: github.com/cloudflare/cfssl
version: 7d88da830aad9d533c2fb8532da23f6a75331b52
subpackages:
- api
- auth
- certdb
- config
- crypto/pkcs7
- csr
- errors
- helpers
- helpers/derhelpers
- info
- initca
- log
- ocsp/config
- signer
- signer/local
- name: github.com/coreos/etcd
version: 5bb9f9591f01d0a3c61d2eb3a3bb281726005b2b
subpackages:
- raft/raftpb
- name: github.com/coreos/go-semver
version: 8ab6407b697782a06568d4b7f1db25550ec2e4c6
subpackages:
- semver
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/dchest/siphash
version: 4ebf1de738443ea7f45f02dc394c4df1942a126d
- name: github.com/dghubble/go-twitter
version: c4115fa44a928413e0b857e0eb47376ffde3a61a
subpackages:
- twitter
- name: github.com/dghubble/oauth1
version: 7d51c10e15ca32917b32ce43f6e25840d6951db4
- name: github.com/dgrijalva/jwt-go
version: a539ee1a749a2b895533f979515ac7e6e0f5b650
- name: github.com/docker/cli
version: 139fcd3ee95f37f3ac17b1200fb0a63908cb6781
subpackages:
- cli/config/configfile
- name: github.com/docker/distribution
version: 5f6282db7d65e6d72ad7c2cc66310724a57be716
subpackages:
- digestset
- manifest
- manifest/schema1
- manifest/schema2
- reference
- registry/api/errcode
- registry/api/v2
- registry/client
- registry/client/auth
- registry/client/auth/challenge
- registry/client/transport
- registry/storage/cache
- registry/storage/cache/memory
- name: github.com/docker/docker
version: cdf870bd0b5fa678b10ef2708cca7ad776b4913c
subpackages:
- api/types
- api/types/blkiodev
- api/types/container
- api/types/filters
- api/types/mount
- api/types/network
- api/types/registry
- api/types/strslice
- api/types/swarm
- api/types/swarm/runtime
- api/types/versions
- daemon/cluster/convert
- opts
- pkg/archive
- pkg/fileutils
- pkg/homedir
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/mount
- pkg/namesgenerator
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/system
- pkg/term
- pkg/term/windows
- name: github.com/docker/go-connections
version: 3ede32e2033de7505e6500d6c868c2b9ed9f169d
subpackages:
- nat
- name: github.com/docker/go-events
version: 9461782956ad83b30282bf90e31fa6a70c255ba9
- name: github.com/docker/go-units
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
- name: github.com/docker/libkv
version: 93ab0e6c056d325dfbb11e1d58a3b4f5f62e7f3c
subpackages:
- store
- name: github.com/docker/libnetwork
version: 6d098467ec58038b68620a3c2c418936661efa64
subpackages:
- datastore
- discoverapi
- types
- name: github.com/docker/libtrust
version: aabc10ec26b754e797f9028f4589c5b7bd90dc20
- name: github.com/docker/swarmkit
version: bd7bafb8a61de1f5f23c8215ce7b9ecbcb30ff21
subpackages:
- api
- api/deepcopy
- api/equality
- api/genericresource
- api/naming
- ca
- connectionbroker
- identity
- ioutils
- log
- manager/raftselector
- manager/state
- manager/state/store
- protobuf/plugin
- remotes
- watch
- watch/queue
- name: github.com/eapache/go-resiliency
version: b1fe83b5b03f624450823b751b662259ffc6af70
subpackages:
- breaker
- name: github.com/eapache/go-xerial-snappy
version: bb955e01b9346ac19dc29eb16586c90ded99a98c
- name: github.com/eapache/queue
version: 44cc805cf13205b55f69e14bcb69867d1ae92f98
- name: github.com/emicklei/go-restful
version: ff4f55a206334ef123e4f79bbf348980da81ca46
subpackages:
- log
- name: github.com/emicklei/go-restful-swagger12
version: dcef7f55730566d41eae5db10e7d6981829720f6
- name: github.com/fnproject/fn_go
version: 7ce3bb2e624df60cdfbfc1ee5483f6df80bb2b1b
subpackages:
- client
- client/apps
- client/call
- client/operations
- client/routes
- models
- name: github.com/fsouza/go-dockerclient
version: 98edf3edfae6a6500fecc69d2bcccf1302544004
- name: github.com/garyburd/redigo
version: 70e1b1943d4fc9c56791abaa6f4d1e727b9ab925
subpackages:
- internal
- redis
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/gin-contrib/cors
version: cf4846e6a636a76237a28d9286f163c132e841bc
- name: github.com/gin-contrib/sse
version: 22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae
- name: github.com/gin-gonic/gin
version: 5afc5b19730118c9b8324fe9dd995d44ec65c81a
subpackages:
- binding
- json
- render
- name: github.com/go-ini/ini
version: c787282c39ac1fc618827141a1f762240def08a3
- name: github.com/go-logfmt/logfmt
version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5
- name: github.com/go-openapi/analysis
version: 8ed83f2ea9f00f945516462951a288eaa68bf0d6
- name: github.com/go-openapi/errors
version: 03cfca65330da08a5a440053faf994a3c682b5bf
- name: github.com/go-openapi/jsonpointer
version: 779f45308c19820f1a69e9a4cd965f496e0da10f
- name: github.com/go-openapi/jsonreference
version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
- name: github.com/go-openapi/loads
version: a80dea3052f00e5f032e860dd7355cd0cc67e24d
subpackages:
- fmts
- name: github.com/go-openapi/runtime
version: d6605b7c17ac3b1033ca794886e6142a4141f5b0
subpackages:
- client
- name: github.com/go-openapi/spec
version: 3faa0055dbbf2110abc1f3b4e3adbb22721e96e7
- name: github.com/go-openapi/strfmt
version: 610b6cacdcde6852f4de68998bd20ce1dac85b22
- name: github.com/go-openapi/swag
version: f3f9494671f93fcff853e3c6e9e948b3eb71e590
- name: github.com/go-openapi/validate
version: 8a82927c942c94794a5cd8b8b50ce2f48a955c0c
- name: github.com/go-sql-driver/mysql
version: 21d7e97c9f760ca685a01ecea202e1c84276daa1
- name: github.com/gogo/protobuf
version: c0656edd0d9eab7c66d1eb0c568f9039345796f7
subpackages:
- gogoproto
- proto
- protoc-gen-gogo/descriptor
- sortkeys
- types
- name: github.com/golang/glog
version: 44145f04b68cf362d9c4df2182967c2275eaefed
- name: github.com/golang/protobuf
version: 4bd1920723d7b7c925de087aa32e2187708897f7
subpackages:
- proto
- ptypes/any
- name: github.com/golang/snappy
version: 553a641470496b2327abcac10b36396bd98e45c9
- name: github.com/google/btree
version: 316fb6d3f031ae8f4d457c6c5186b9e3ded70435
- name: github.com/google/certificate-transparency-go
version: 0dac42a6ed448ba220ee315abfaa6d26fd5fc9bb
repo: https://github.com/google/certificate-transparency-go
subpackages:
- asn1
- client
- jsonclient
- tls
- x509
- x509/pkix
- name: github.com/google/gofuzz
version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/mux
version: 24fca303ac6da784b9e8269f724ddeb0b2eea5e7
- name: github.com/grpc-ecosystem/go-grpc-prometheus
version: 6b7015e65d366bf3f19b2b2a000a831940f0f7e0
- name: github.com/hashicorp/go-immutable-radix
version: 8aac2701530899b64bdea735a1de8da899815220
- name: github.com/hashicorp/go-memdb
version: ec43fcf8f202880feb35d2abb40a570c1f4172e9
- name: github.com/hashicorp/golang-lru
version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4
subpackages:
- simplelru
- name: github.com/jmoiron/jsonq
version: e874b168d07ecc7808bc950a17998a8aa3141d82
- name: github.com/jmoiron/sqlx
version: d9bd385d68c068f1fabb5057e3dedcbcbb039d0f
subpackages:
- reflectx
- name: github.com/json-iterator/go
version: fdfe0b9a69118ff692d6e1005e9de7e0cffb7d6b
- name: github.com/juju/ratelimit
version: 5b9ff866471762aa2ab2dced63c9fb6f53921342
- name: github.com/kr/logfmt
version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0
- name: github.com/lib/pq
version: 23da1db4f16d9658a86ae9b717c245fc078f10f1
subpackages:
- oid
- name: github.com/mailru/easyjson
version: 32fa128f234d041f196a9f3e0fea5ac9772c08e1
subpackages:
- buffer
- jlexer
- jwriter
- name: github.com/mattes/migrate
version: 5b98c13eff7657ab49a1a5f705b72f961d7fc558
subpackages:
- database
- source
- name: github.com/mattn/go-isatty
version: fc9e8d8ef48496124e79ae0df75490096eccf6fe
- name: github.com/mattn/go-sqlite3
version: 05548ff55570cdb9ac72ff4a25a3b5e77a6fb7e5
- name: github.com/matttproud/golang_protobuf_extensions
version: c12348ce28de40eed0136aa2b644d0ee0650e56c
subpackages:
- pbutil
- name: github.com/Microsoft/go-winio
version: 78439966b38d69bf38227fbf57ac8a6fee70f69a
- name: github.com/minio/go-homedir
version: 21304a94172ae3a09dee2cd86a12fb6f842138c7
- name: github.com/minio/minio-go
version: a62e2045a5d3a6630dbb7040260994583ac56b10
subpackages:
- pkg/credentials
- pkg/encrypt
- pkg/policy
- pkg/s3signer
- pkg/s3utils
- pkg/set
- name: github.com/mitchellh/mapstructure
version: d0303fe809921458f417bcf828397a65db30a7e4
- name: github.com/Nvveen/Gotty
version: cd527374f1e5bff4938207604a14f2e38a9cf512
- name: github.com/opencontainers/go-digest
version: 279bed98673dd5bef374d3b6e4b09e2af76183bf
- name: github.com/opencontainers/image-spec
version: ebd93fd0782379ca3d821f0fa74f0651a9347a3e
subpackages:
- specs-go
- specs-go/v1
- name: github.com/opencontainers/runc
version: ae2948042b08ad3d6d13cd09f40a50ffff4fc688
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/opentracing-contrib/go-observer
version: a52f2342449246d5bcc273e65cbdcfa5f7d6c63c
- name: github.com/opentracing/opentracing-go
version: 8ebe5d4e236eed9fd88e593c288bfb804d630b8c
subpackages:
- ext
- log
- name: github.com/openzipkin/zipkin-go-opentracing
version: 9c88fa03bfdfaa5fec7cd1b40f3d10ec15c15fc6
subpackages:
- flag
- thrift/gen-go/scribe
- thrift/gen-go/zipkincore
- types
- wire
- name: github.com/patrickmn/go-cache
version: a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0
- name: github.com/pierrec/lz4
version: 08c27939df1bd95e881e2c2367a749964ad1fceb
- name: github.com/pierrec/xxHash
version: a0006b13c722f7f12368c00a3d3c2ae8a999a0c6
subpackages:
- xxHash32
- name: github.com/pkg/errors
version: 2b3a18b5f0fb6b4f9190549597d3f962c02bc5eb
- name: github.com/prometheus/client_golang
version: c5b7fccd204277076155f10851dad72b76a49317
subpackages:
- prometheus
- prometheus/promhttp
- name: github.com/prometheus/client_model
version: 6f3806018612930941127f2a7c6c453ba2c527d2
subpackages:
- go
- name: github.com/prometheus/common
version: 2f17f4a9d485bf34b4bfaccc273805040e4f86c8
subpackages:
- expfmt
- internal/bitbucket.org/ww/goautoneg
- model
- name: github.com/prometheus/procfs
version: a1dba9ce8baed984a2495b658c82687f8157b98f
subpackages:
- xfs
- name: github.com/PuerkitoBio/purell
version: 8a290539e2e8629dbc4e6bad948158f790ec31f4
- name: github.com/PuerkitoBio/urlesc
version: 5bd2802263f21d8788851d5305584c82a5c75d7e
- name: github.com/rcrowley/go-metrics
version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c
- name: github.com/rdallman/migrate
version: bc72eeb997c7334cb5f05f5aefd2d70bc34d71ef
subpackages:
- database/mysql
- database/postgres
- database/sqlite3
- source
- source/go-bindata
- name: github.com/Shopify/sarama
version: 4704a3a8c95920361c47e9a2adec13c3d757c757
- name: github.com/sirupsen/logrus
version: 89742aefa4b206dcf400792f3bd35b542998eb3b
subpackages:
- hooks/syslog
- name: github.com/spf13/pflag
version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
- name: github.com/ugorji/go
version: ded73eae5db7e7a0ef6f55aace87a2873c5d2b74
subpackages:
- codec
- name: golang.org/x/crypto
version: 94eea52f7b742c7cbe0b03b22f0c4c8631ece122
subpackages:
- ocsp
- pkcs12
- pkcs12/internal/rc2
- ssh/terminal
- name: golang.org/x/net
version: a8b9294777976932365dabb6640cf1468d95c70f
subpackages:
- context
- context/ctxhttp
- http2
- http2/hpack
- idna
- internal/timeseries
- lex/httplex
- trace
- name: golang.org/x/sys
version: 8b4580aae2a0dd0c231a45d3ccb8434ff533b840
subpackages:
- unix
- windows
- name: golang.org/x/text
version: 57961680700a5336d15015c8c50686ca5ba362a4
subpackages:
- cases
- internal
- internal/tag
- language
- runes
- secure/bidirule
- secure/precis
- transform
- unicode/bidi
- unicode/norm
- width
- name: google.golang.org/genproto
version: 09f6ed296fc66555a25fe4ce95173148778dfa85
subpackages:
- googleapis/rpc/status
- name: google.golang.org/grpc
version: b8669c35455183da6d5c474ea6e72fbf55183274
subpackages:
- codes
- credentials
- grpclb/grpc_lb_v1
- grpclog
- internal
- keepalive
- metadata
- naming
- peer
- stats
- status
- tap
- transport
- name: gopkg.in/go-playground/validator.v8
version: 5f1438d3fca68893a817e4a66806cea46a9e4ebf
- name: gopkg.in/inf.v0
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
- name: gopkg.in/mgo.v2
version: 3f83fa5005286a7fe593b055f0d7771a7dce4655
subpackages:
- bson
- internal/json
- name: gopkg.in/yaml.v2
version: 53feefa2559fb8dfa8d81baad31be332c97d6c77
- name: k8s.io/apimachinery
version: 1fd2e63a9a370677308a42f24fd40c86438afddf
subpackages:
- pkg/api/equality
- pkg/api/errors
- pkg/api/meta
- pkg/api/resource
- pkg/apimachinery
- pkg/apimachinery/announced
- pkg/apimachinery/registered
- pkg/apis/meta/v1
- pkg/apis/meta/v1/unstructured
- pkg/apis/meta/v1alpha1
- pkg/conversion
- pkg/conversion/queryparams
- pkg/conversion/unstructured
- pkg/fields
- pkg/labels
- pkg/openapi
- pkg/runtime
- pkg/runtime/schema
- pkg/runtime/serializer
- pkg/runtime/serializer/json
- pkg/runtime/serializer/protobuf
- pkg/runtime/serializer/recognizer
- pkg/runtime/serializer/streaming
- pkg/runtime/serializer/versioning
- pkg/selection
- pkg/types
- pkg/util/clock
- pkg/util/diff
- pkg/util/errors
- pkg/util/framer
- pkg/util/intstr
- pkg/util/json
- pkg/util/net
- pkg/util/rand
- pkg/util/runtime
- pkg/util/sets
- pkg/util/validation
- pkg/util/validation/field
- pkg/util/wait
- pkg/util/yaml
- pkg/version
- pkg/watch
- third_party/forked/golang/reflect
- name: k8s.io/client-go
version: d92e8497f71b7b4e0494e5bd204b48d34bd6f254
subpackages:
- discovery
- kubernetes
- kubernetes/scheme
- kubernetes/typed/admissionregistration/v1alpha1
- kubernetes/typed/apps/v1beta1
- kubernetes/typed/authentication/v1
- kubernetes/typed/authentication/v1beta1
- kubernetes/typed/authorization/v1
- kubernetes/typed/authorization/v1beta1
- kubernetes/typed/autoscaling/v1
- kubernetes/typed/autoscaling/v2alpha1
- kubernetes/typed/batch/v1
- kubernetes/typed/batch/v2alpha1
- kubernetes/typed/certificates/v1beta1
- kubernetes/typed/core/v1
- kubernetes/typed/extensions/v1beta1
- kubernetes/typed/networking/v1
- kubernetes/typed/policy/v1beta1
- kubernetes/typed/rbac/v1alpha1
- kubernetes/typed/rbac/v1beta1
- kubernetes/typed/settings/v1alpha1
- kubernetes/typed/storage/v1
- kubernetes/typed/storage/v1beta1
- pkg/api
- pkg/api/v1
- pkg/api/v1/ref
- pkg/apis/admissionregistration
- pkg/apis/admissionregistration/v1alpha1
- pkg/apis/apps
- pkg/apis/apps/v1beta1
- pkg/apis/authentication
- pkg/apis/authentication/v1
- pkg/apis/authentication/v1beta1
- pkg/apis/authorization
- pkg/apis/authorization/v1
- pkg/apis/authorization/v1beta1
- pkg/apis/autoscaling
- pkg/apis/autoscaling/v1
- pkg/apis/autoscaling/v2alpha1
- pkg/apis/batch
- pkg/apis/batch/v1
- pkg/apis/batch/v2alpha1
- pkg/apis/certificates
- pkg/apis/certificates/v1beta1
- pkg/apis/extensions
- pkg/apis/extensions/v1beta1
- pkg/apis/networking
- pkg/apis/networking/v1
- pkg/apis/policy
- pkg/apis/policy/v1beta1
- pkg/apis/rbac
- pkg/apis/rbac/v1alpha1
- pkg/apis/rbac/v1beta1
- pkg/apis/settings
- pkg/apis/settings/v1alpha1
- pkg/apis/storage
- pkg/apis/storage/v1
- pkg/apis/storage/v1beta1
- pkg/util
- pkg/util/parsers
- pkg/version
- rest
- rest/watch
- tools/clientcmd/api
- tools/metrics
- transport
- util/cert
- util/flowcontrol
- util/integer
testImports: []

View File

@@ -1,94 +0,0 @@
package: github.com/fnproject/fn
excludeDirs:
- cli
import:
- package: golang.org/x/crypto
version: master
subpackages:
- pkcs12
- package: github.com/fnproject/fn_go
version: ^0.2.0
subpackages:
- models
- package: github.com/sirupsen/logrus
version: 89742aefa4b206dcf400792f3bd35b542998eb3b
- package: github.com/amir/raidman
- package: github.com/boltdb/bolt
- package: github.com/cactus/go-statsd-client
subpackages:
- statsd
- package: github.com/dchest/siphash
- package: github.com/dghubble/go-twitter
subpackages:
- twitter
- package: github.com/dghubble/oauth1
- package: github.com/dgrijalva/jwt-go
- package: github.com/docker/cli
version: 139fcd3ee95f37f3ac17b1200fb0a63908cb6781
subpackages:
- cli/config/configfile
- package: github.com/docker/distribution
version: 5f6282db7d65e6d72ad7c2cc66310724a57be716
- package: github.com/fsouza/go-dockerclient
- package: github.com/garyburd/redigo
subpackages:
- redis
- package: github.com/gin-gonic/gin
- package: github.com/rdallman/migrate
version: bc72eeb997c7334cb5f05f5aefd2d70bc34d71ef
- package: github.com/go-openapi/errors
- package: github.com/go-openapi/loads
subpackages:
- fmts
- package: github.com/go-openapi/runtime
subpackages:
- client
- package: github.com/go-openapi/spec
- package: github.com/go-openapi/strfmt
- package: github.com/go-openapi/swag
- package: github.com/go-openapi/validate
- package: github.com/go-sql-driver/mysql
version: 21d7e97c9f760ca685a01ecea202e1c84276daa1
- package: github.com/google/btree
- package: github.com/jmoiron/jsonq
- package: github.com/lib/pq
- package: github.com/docker/docker
version: cdf870bd0b5fa678b10ef2708cca7ad776b4913c
- package: github.com/pkg/errors
- package: github.com/jmoiron/sqlx
- package: github.com/mattn/go-sqlite3
- package: github.com/minio/minio-go
- package: github.com/opentracing/opentracing-go
- package: github.com/openzipkin/zipkin-go-opentracing
- package: github.com/opencontainers/go-digest
version: 279bed98673dd5bef374d3b6e4b09e2af76183bf
- package: github.com/opencontainers/runc
version: ae2948042b08ad3d6d13cd09f40a50ffff4fc688
- package: github.com/Azure/go-ansiterm
version: 19f72df4d05d31cbe1c56bfc8045c96babff6c7e
- package: github.com/prometheus/common
version: 2f17f4a9d485bf34b4bfaccc273805040e4f86c8
- package: github.com/prometheus/client_golang
- package: github.com/gin-contrib/cors
version: ~1.2.0
- package: k8s.io/client-go
version: ^v4.0.0
subpackages:
- kubernetes
- package: github.com/emicklei/go-restful-swagger12
- package: golang.org/x/sys
version: master
subpackages:
- unix
- package: golang.org/x/net
version: master
subpackages:
- http2
- package: golang.org/x/text
version: master
- package: github.com/mailru/easyjson
version: master
subpackages:
- jwriter
testImport:
- package: github.com/patrickmn/go-cache

View File

@@ -1,73 +0,0 @@
Raidman
=======
Go Riemann client
```go
package main
import (
"github.com/amir/raidman"
)
func main() {
c, err := raidman.Dial("tcp", "localhost:5555")
if err != nil {
panic(err)
}
var event = &raidman.Event{
State: "success",
Host: "raidman",
Service: "raidman-sample",
Metric: 100,
Ttl: 10,
}
// send one event
err = c.Send(event)
if err != nil {
panic(err)
}
// send multiple events at once
err = c.SendMulti([]*raidman.Event{
&raidman.Event{
State: "success",
Host: "raidman",
Service: "raidman-sample",
Metric: 100,
Ttl: 10,
},
&raidman.Event{
State: "failure",
Host: "raidman",
Service: "raidman-sample",
Metric: 100,
Ttl: 10,
},
&raidman.Event{
State: "success",
Host: "raidman",
Service: "raidman-sample",
Metric: 100,
Ttl: 10,
},
})
if err != nil {
panic(err)
}
events, err := c.Query("host = \"raidman\"")
if err != nil {
panic(err)
}
if len(events) < 1 {
panic("Submitted event not found")
}
c.Close()
}
```

View File

@@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@@ -1,6 +0,0 @@
proto.pb.go: proto.proto
mkdir -p _pb
protoc --go_out=_pb $<
cat _pb/$@\
|gofmt >$@
rm -rf _pb

View File

@@ -1,273 +0,0 @@
// Code generated by protoc-gen-go.
// source: proto.proto
// DO NOT EDIT!
package proto
import proto1 "github.com/golang/protobuf/proto"
import json "encoding/json"
import math "math"
// Reference proto, json, and math imports to suppress error if they are not otherwise used.
var _ = proto1.Marshal
var _ = &json.SyntaxError{}
var _ = math.Inf
type State struct {
Time *int64 `protobuf:"varint,1,opt,name=time" json:"time,omitempty"`
State *string `protobuf:"bytes,2,opt,name=state" json:"state,omitempty"`
Service *string `protobuf:"bytes,3,opt,name=service" json:"service,omitempty"`
Host *string `protobuf:"bytes,4,opt,name=host" json:"host,omitempty"`
Description *string `protobuf:"bytes,5,opt,name=description" json:"description,omitempty"`
Once *bool `protobuf:"varint,6,opt,name=once" json:"once,omitempty"`
Tags []string `protobuf:"bytes,7,rep,name=tags" json:"tags,omitempty"`
Ttl *float32 `protobuf:"fixed32,8,opt,name=ttl" json:"ttl,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (this *State) Reset() { *this = State{} }
func (this *State) String() string { return proto1.CompactTextString(this) }
func (*State) ProtoMessage() {}
func (this *State) GetTime() int64 {
if this != nil && this.Time != nil {
return *this.Time
}
return 0
}
func (this *State) GetState() string {
if this != nil && this.State != nil {
return *this.State
}
return ""
}
func (this *State) GetService() string {
if this != nil && this.Service != nil {
return *this.Service
}
return ""
}
func (this *State) GetHost() string {
if this != nil && this.Host != nil {
return *this.Host
}
return ""
}
func (this *State) GetDescription() string {
if this != nil && this.Description != nil {
return *this.Description
}
return ""
}
func (this *State) GetOnce() bool {
if this != nil && this.Once != nil {
return *this.Once
}
return false
}
func (this *State) GetTags() []string {
if this != nil {
return this.Tags
}
return nil
}
func (this *State) GetTtl() float32 {
if this != nil && this.Ttl != nil {
return *this.Ttl
}
return 0
}
type Event struct {
Time *int64 `protobuf:"varint,1,opt,name=time" json:"time,omitempty"`
State *string `protobuf:"bytes,2,opt,name=state" json:"state,omitempty"`
Service *string `protobuf:"bytes,3,opt,name=service" json:"service,omitempty"`
Host *string `protobuf:"bytes,4,opt,name=host" json:"host,omitempty"`
Description *string `protobuf:"bytes,5,opt,name=description" json:"description,omitempty"`
Tags []string `protobuf:"bytes,7,rep,name=tags" json:"tags,omitempty"`
Ttl *float32 `protobuf:"fixed32,8,opt,name=ttl" json:"ttl,omitempty"`
Attributes []*Attribute `protobuf:"bytes,9,rep,name=attributes" json:"attributes,omitempty"`
MetricSint64 *int64 `protobuf:"zigzag64,13,opt,name=metric_sint64" json:"metric_sint64,omitempty"`
MetricD *float64 `protobuf:"fixed64,14,opt,name=metric_d" json:"metric_d,omitempty"`
MetricF *float32 `protobuf:"fixed32,15,opt,name=metric_f" json:"metric_f,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (this *Event) Reset() { *this = Event{} }
func (this *Event) String() string { return proto1.CompactTextString(this) }
func (*Event) ProtoMessage() {}
func (this *Event) GetTime() int64 {
if this != nil && this.Time != nil {
return *this.Time
}
return 0
}
func (this *Event) GetState() string {
if this != nil && this.State != nil {
return *this.State
}
return ""
}
func (this *Event) GetService() string {
if this != nil && this.Service != nil {
return *this.Service
}
return ""
}
func (this *Event) GetHost() string {
if this != nil && this.Host != nil {
return *this.Host
}
return ""
}
func (this *Event) GetDescription() string {
if this != nil && this.Description != nil {
return *this.Description
}
return ""
}
func (this *Event) GetTags() []string {
if this != nil {
return this.Tags
}
return nil
}
func (this *Event) GetTtl() float32 {
if this != nil && this.Ttl != nil {
return *this.Ttl
}
return 0
}
func (this *Event) GetAttributes() []*Attribute {
if this != nil {
return this.Attributes
}
return nil
}
func (this *Event) GetMetricSint64() int64 {
if this != nil && this.MetricSint64 != nil {
return *this.MetricSint64
}
return 0
}
func (this *Event) GetMetricD() float64 {
if this != nil && this.MetricD != nil {
return *this.MetricD
}
return 0
}
func (this *Event) GetMetricF() float32 {
if this != nil && this.MetricF != nil {
return *this.MetricF
}
return 0
}
type Query struct {
String_ *string `protobuf:"bytes,1,opt,name=string" json:"string,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (this *Query) Reset() { *this = Query{} }
func (this *Query) String() string { return proto1.CompactTextString(this) }
func (*Query) ProtoMessage() {}
func (this *Query) GetString_() string {
if this != nil && this.String_ != nil {
return *this.String_
}
return ""
}
type Msg struct {
Ok *bool `protobuf:"varint,2,opt,name=ok" json:"ok,omitempty"`
Error *string `protobuf:"bytes,3,opt,name=error" json:"error,omitempty"`
States []*State `protobuf:"bytes,4,rep,name=states" json:"states,omitempty"`
Query *Query `protobuf:"bytes,5,opt,name=query" json:"query,omitempty"`
Events []*Event `protobuf:"bytes,6,rep,name=events" json:"events,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (this *Msg) Reset() { *this = Msg{} }
func (this *Msg) String() string { return proto1.CompactTextString(this) }
func (*Msg) ProtoMessage() {}
func (this *Msg) GetOk() bool {
if this != nil && this.Ok != nil {
return *this.Ok
}
return false
}
func (this *Msg) GetError() string {
if this != nil && this.Error != nil {
return *this.Error
}
return ""
}
func (this *Msg) GetStates() []*State {
if this != nil {
return this.States
}
return nil
}
func (this *Msg) GetQuery() *Query {
if this != nil {
return this.Query
}
return nil
}
func (this *Msg) GetEvents() []*Event {
if this != nil {
return this.Events
}
return nil
}
type Attribute struct {
Key *string `protobuf:"bytes,1,req,name=key" json:"key,omitempty"`
Value *string `protobuf:"bytes,2,opt,name=value" json:"value,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (this *Attribute) Reset() { *this = Attribute{} }
func (this *Attribute) String() string { return proto1.CompactTextString(this) }
func (*Attribute) ProtoMessage() {}
func (this *Attribute) GetKey() string {
if this != nil && this.Key != nil {
return *this.Key
}
return ""
}
func (this *Attribute) GetValue() string {
if this != nil && this.Value != nil {
return *this.Value
}
return ""
}
func init() {
}

View File

@@ -1,45 +0,0 @@
option java_package = "com.aphyr.riemann";
option java_outer_classname = "Proto";
message State {
optional int64 time = 1;
optional string state = 2;
optional string service = 3;
optional string host = 4;
optional string description = 5;
optional bool once = 6;
repeated string tags = 7;
optional float ttl = 8;
}
message Event {
optional int64 time = 1;
optional string state = 2;
optional string service = 3;
optional string host = 4;
optional string description = 5;
repeated string tags = 7;
optional float ttl = 8;
repeated Attribute attributes = 9;
optional sint64 metric_sint64 = 13;
optional double metric_d = 14;
optional float metric_f = 15;
}
message Query {
optional string string = 1;
}
message Msg {
optional bool ok = 2;
optional string error = 3;
repeated State states = 4;
optional Query query = 5;
repeated Event events = 6;
}
message Attribute {
required string key = 1;
optional string value = 2;
}

View File

@@ -1,341 +0,0 @@
// Go Riemann client
package raidman
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"reflect"
"sync"
"time"
"github.com/amir/raidman/proto"
pb "github.com/golang/protobuf/proto"
"golang.org/x/net/proxy"
)
type network interface {
Send(message *proto.Msg, conn net.Conn) (*proto.Msg, error)
}
type tcp struct{}
type udp struct{}
// Client represents a connection to a Riemann server
type Client struct {
sync.Mutex
net network
connection net.Conn
timeout time.Duration
}
// An Event represents a single Riemann event
type Event struct {
Ttl float32 `json:"ttl,omitempty"`
Time int64 `json:"time,omitempty"`
Tags []string `json:"tags,omitempty"`
Host string `json:"host,omitempty"` // Defaults to os.Hostname()
State string `json:"state,omitempty"`
Service string `json:"service,omitempty"`
Metric interface{} `json:"metric,omitempty"` // Could be Int, Float32, Float64
Description string `json:"description,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// Dial establishes a connection to a Riemann server at addr, on the network
// netwrk, with a timeout of timeout
//
// Known networks are "tcp", "tcp4", "tcp6", "udp", "udp4", and "udp6".
func DialWithTimeout(netwrk, addr string, timeout time.Duration) (c *Client, err error) {
c = new(Client)
var cnet network
switch netwrk {
case "tcp", "tcp4", "tcp6":
cnet = new(tcp)
case "udp", "udp4", "udp6":
cnet = new(udp)
default:
return nil, fmt.Errorf("dial %q: unsupported network %q", netwrk, netwrk)
}
dialer, err := newDialer()
if err != nil {
return nil, err
}
c.net = cnet
c.timeout = timeout
c.connection, err = dialer.Dial(netwrk, addr)
if err != nil {
return nil, err
}
return c, nil
}
func newDialer() (proxy.Dialer, error) {
var proxyUrl = os.Getenv("RIEMANN_PROXY")
var dialer proxy.Dialer = proxy.Direct
// Get a proxy Dialer that will create the connection on our
// behalf via the SOCKS5 proxy. Specify the authentication
// and re-create the dialer/transport/client if tor's
// IsolateSOCKSAuth is needed.
if len(proxyUrl) > 0 {
u, err := url.Parse(proxyUrl)
if err != nil {
return nil, fmt.Errorf("failed to obtain proxy dialer: %v\n", err)
}
if dialer, err = proxy.FromURL(u, dialer); err != nil {
return nil, fmt.Errorf("failed to parse " + proxyUrl + " as a proxy: " + err.Error())
}
}
return dialer, nil
}
// Dial establishes a connection to a Riemann server at addr, on the network
// netwrk.
//
// Known networks are "tcp", "tcp4", "tcp6", "udp", "udp4", and "udp6".
func Dial(netwrk, addr string) (c *Client, err error) {
return DialWithTimeout(netwrk, addr, 0)
}
func (network *tcp) Send(message *proto.Msg, conn net.Conn) (*proto.Msg, error) {
msg := &proto.Msg{}
data, err := pb.Marshal(message)
if err != nil {
return msg, err
}
b := new(bytes.Buffer)
if err = binary.Write(b, binary.BigEndian, uint32(len(data))); err != nil {
return msg, err
}
if _, err = conn.Write(b.Bytes()); err != nil {
return msg, err
}
if _, err = conn.Write(data); err != nil {
return msg, err
}
var header uint32
if err = binary.Read(conn, binary.BigEndian, &header); err != nil {
return msg, err
}
response := make([]byte, header)
if err = readFully(conn, response); err != nil {
return msg, err
}
if err = pb.Unmarshal(response, msg); err != nil {
return msg, err
}
if msg.GetOk() != true {
return msg, errors.New(msg.GetError())
}
return msg, nil
}
func readFully(r io.Reader, p []byte) error {
for len(p) > 0 {
n, err := r.Read(p)
p = p[n:]
if err != nil {
return err
}
}
return nil
}
func (network *udp) Send(message *proto.Msg, conn net.Conn) (*proto.Msg, error) {
data, err := pb.Marshal(message)
if err != nil {
return nil, err
}
if _, err = conn.Write(data); err != nil {
return nil, err
}
return nil, nil
}
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Map:
return v.IsNil()
case reflect.Slice:
zero := true
for i := 0; i < v.Len(); i++ {
zero = zero && isZero(v.Index(i))
}
return zero
}
zero := reflect.Zero(v.Type())
return v.Interface() == zero.Interface()
}
func eventToPbEvent(event *Event) (*proto.Event, error) {
var e proto.Event
if event.Host == "" {
event.Host, _ = os.Hostname()
}
t := reflect.ValueOf(&e).Elem()
s := reflect.ValueOf(event).Elem()
typeOfEvent := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
value := reflect.ValueOf(f.Interface())
if !isZero(f) {
name := typeOfEvent.Field(i).Name
switch name {
case "State", "Service", "Host", "Description":
tmp := reflect.ValueOf(pb.String(value.String()))
t.FieldByName(name).Set(tmp)
case "Ttl":
tmp := reflect.ValueOf(pb.Float32(float32(value.Float())))
t.FieldByName(name).Set(tmp)
case "Time":
tmp := reflect.ValueOf(pb.Int64(value.Int()))
t.FieldByName(name).Set(tmp)
case "Tags":
tmp := reflect.ValueOf(value.Interface().([]string))
t.FieldByName(name).Set(tmp)
case "Metric":
switch reflect.TypeOf(f.Interface()).Kind() {
case reflect.Int, reflect.Int64:
tmp := reflect.ValueOf(pb.Int64(int64(value.Int())))
t.FieldByName("MetricSint64").Set(tmp)
case reflect.Uint64:
tmp := reflect.ValueOf(pb.Int64(int64(value.Uint())))
t.FieldByName("MetricSint64").Set(tmp)
case reflect.Float32:
tmp := reflect.ValueOf(pb.Float32(float32(value.Float())))
t.FieldByName("MetricF").Set(tmp)
case reflect.Float64:
tmp := reflect.ValueOf(pb.Float64(value.Float()))
t.FieldByName("MetricD").Set(tmp)
default:
return nil, fmt.Errorf("Metric of invalid type (type %v)",
reflect.TypeOf(f.Interface()).Kind())
}
case "Attributes":
var attrs []*proto.Attribute
for k, v := range value.Interface().(map[string]string) {
// Copy k,v so we can take
// pointers to the new
// temporaries
k_, v_ := k, v
attrs = append(attrs, &proto.Attribute{
Key: &k_,
Value: &v_,
})
}
t.FieldByName(name).Set(reflect.ValueOf(attrs))
}
}
}
return &e, nil
}
func pbEventsToEvents(pbEvents []*proto.Event) []Event {
var events []Event
for _, event := range pbEvents {
e := Event{
State: event.GetState(),
Service: event.GetService(),
Host: event.GetHost(),
Description: event.GetDescription(),
Ttl: event.GetTtl(),
Time: event.GetTime(),
Tags: event.GetTags(),
}
if event.MetricF != nil {
e.Metric = event.GetMetricF()
} else if event.MetricD != nil {
e.Metric = event.GetMetricD()
} else {
e.Metric = event.GetMetricSint64()
}
if event.Attributes != nil {
e.Attributes = make(map[string]string, len(event.GetAttributes()))
for _, attr := range event.GetAttributes() {
e.Attributes[attr.GetKey()] = attr.GetValue()
}
}
events = append(events, e)
}
return events
}
// Send sends an event to Riemann
func (c *Client) Send(event *Event) error {
return c.SendMulti([]*Event{event})
}
// SendMulti sends multiple events to Riemann
func (c *Client) SendMulti(events []*Event) error {
message := &proto.Msg{}
for _, event := range events {
e, err := eventToPbEvent(event)
if err != nil {
return err
}
message.Events = append(message.Events, e)
}
c.Lock()
defer c.Unlock()
if c.timeout > 0 {
err := c.connection.SetDeadline(time.Now().Add(c.timeout))
if err != nil {
return err
}
}
_, err := c.net.Send(message, c.connection)
if err != nil {
return err
}
return nil
}
// Query returns a list of events matched by query
func (c *Client) Query(q string) ([]Event, error) {
switch c.net.(type) {
case *udp:
return nil, errors.New("Querying over UDP is not supported")
}
query := &proto.Query{}
query.String_ = pb.String(q)
message := &proto.Msg{}
message.Query = query
c.Lock()
defer c.Unlock()
response, err := c.net.Send(message, c.connection)
if err != nil {
return nil, err
}
return pbEventsToEvents(response.GetEvents()), nil
}
// Close closes the connection to Riemann
func (c *Client) Close() error {
c.Lock()
defer c.Unlock()
return c.connection.Close()
}

View File

@@ -1,286 +0,0 @@
package raidman
import (
"fmt"
"os"
"reflect"
"testing"
)
func TestTCP(t *testing.T) {
c, err := Dial("tcp", "localhost:5555")
if err != nil {
t.Fatal(err.Error())
}
var event = &Event{
State: "success",
Host: "raidman",
Service: "tcp",
Metric: 42,
Ttl: 1,
Tags: []string{"tcp", "test", "raidman"},
Attributes: map[string]string{"type": "test"},
}
err = c.Send(event)
if err != nil {
t.Error(err.Error())
}
events, err := c.Query("tagged \"test\"")
if err != nil {
t.Error(err.Error())
}
if len(events) < 1 {
t.Error("Submitted event not found")
}
testAttributeExists := false
for _, event := range events {
if val, ok := event.Attributes["type"]; ok && val == "test" {
testAttributeExists = true
}
}
if !testAttributeExists {
t.Error("Attribute \"type\" is missing")
}
c.Close()
}
func TestMultiTCP(t *testing.T) {
c, err := Dial("tcp", "localhost:5555")
if err != nil {
t.Fatal(err.Error())
}
err = c.SendMulti([]*Event{
&Event{
State: "success",
Host: "raidman",
Service: "tcp-multi-1",
Metric: 42,
Ttl: 1,
Tags: []string{"tcp", "test", "raidman", "multi"},
Attributes: map[string]string{"type": "test"},
},
&Event{
State: "success",
Host: "raidman",
Service: "tcp-multi-2",
Metric: 42,
Ttl: 1,
Tags: []string{"tcp", "test", "raidman", "multi"},
Attributes: map[string]string{"type": "test"},
},
})
if err != nil {
t.Error(err.Error())
}
events, err := c.Query("tagged \"test\" and tagged \"multi\"")
if err != nil {
t.Error(err.Error())
}
if len(events) != 2 {
t.Error("Submitted event not found")
}
c.Close()
}
func TestMetricIsInt64(t *testing.T) {
c, err := Dial("tcp", "localhost:5555")
if err != nil {
t.Fatal(err.Error())
}
var int64metric int64 = 9223372036854775807
var event = &Event{
State: "success",
Host: "raidman",
Service: "tcp",
Metric: int64metric,
Ttl: 1,
Tags: []string{"tcp", "test", "raidman"},
Attributes: map[string]string{"type": "test"},
}
err = c.Send(event)
if err != nil {
t.Error(err.Error())
}
}
func TestUDP(t *testing.T) {
c, err := Dial("udp", "localhost:5555")
if err != nil {
t.Fatal(err.Error())
}
var event = &Event{
State: "warning",
Host: "raidman",
Service: "udp",
Metric: 3.4,
Ttl: 10.7,
}
err = c.Send(event)
if err != nil {
t.Error(err.Error())
}
c.Close()
}
func TestTCPWithoutHost(t *testing.T) {
c, err := Dial("tcp", "localhost:5555")
if err != nil {
t.Fatal(err.Error())
}
defer c.Close()
var event = &Event{
State: "success",
Service: "tcp-host-not-set",
Ttl: 5,
}
err = c.Send(event)
if err != nil {
t.Error(err.Error())
}
events, err := c.Query("service = \"tcp-host-not-set\"")
if err != nil {
t.Error(err.Error())
}
if len(events) < 1 {
t.Error("Submitted event not found")
}
for _, e := range events {
if e.Host == "" {
t.Error("Default host name is not set")
}
}
}
func TestIsZero(t *testing.T) {
event := &Event{
Time: 1,
}
elem := reflect.ValueOf(event).Elem()
eventType := elem.Type()
for i := 0; i < elem.NumField(); i++ {
field := elem.Field(i)
name := eventType.Field(i).Name
if name == "Time" {
if isZero(field) {
t.Error("Time should not be zero")
}
} else {
if !isZero(field) {
t.Errorf("%s should be zero", name)
}
}
}
}
func TestDialer(t *testing.T) {
proxyAddr := "localhost:9999"
os.Setenv("RIEMANN_PROXY", "socks5://"+proxyAddr)
defer os.Unsetenv("RIEMANN_PROXY")
dialer, err := newDialer()
if err != nil {
t.Error(err.Error())
}
val := reflect.Indirect(reflect.ValueOf(dialer))
// this is a horrible hack but proxy.Dialer exports nothing.
addr := fmt.Sprintf("%s", val.FieldByName("addr"))
if addr != proxyAddr {
t.Errorf("RIEMANN_PROXY is set and is %s but dialer's proxy is %s", proxyAddr, addr)
}
}
func BenchmarkTCP(b *testing.B) {
c, err := Dial("tcp", "localhost:5555")
var event = &Event{
State: "good",
Host: "raidman",
Service: "benchmark",
}
if err == nil {
for i := 0; i < b.N; i++ {
c.Send(event)
}
}
c.Close()
}
func BenchmarkUDP(b *testing.B) {
c, err := Dial("udp", "localhost:5555")
var event = &Event{
State: "good",
Host: "raidman",
Service: "benchmark",
}
if err == nil {
for i := 0; i < b.N; i++ {
c.Send(event)
}
}
c.Close()
}
func BenchmarkConcurrentTCP(b *testing.B) {
c, err := Dial("tcp", "localhost:5555")
var event = &Event{
Host: "raidman",
Service: "tcp_concurrent",
Tags: []string{"concurrent", "tcp", "benchmark"},
}
ch := make(chan int, b.N)
for i := 0; i < b.N; i++ {
go func(metric int) {
event.Metric = metric
err = c.Send(event)
ch <- i
}(i)
}
<-ch
c.Close()
}
func BenchmarkConcurrentUDP(b *testing.B) {
c, err := Dial("udp", "localhost:5555")
var event = &Event{
Host: "raidman",
Service: "udp_concurrent",
Tags: []string{"concurrent", "udp", "benchmark"},
}
ch := make(chan int, b.N)
for i := 0; i < b.N; i++ {
go func(metric int) {
event.Metric = metric
err = c.Send(event)
ch <- i
}(i)
}
<-ch
c.Close()
}

View File

@@ -1,10 +0,0 @@
language: go
sudo: false
script: go test -v -cpu=1,2 ./...
go:
- 1.4
- 1.5
- 1.6
- 1.7
- 1.8
- 1.9

View File

@@ -1,49 +0,0 @@
Changelog
=========
## head
* Fix leak on sender create with unresolvable destination (GH-34).
## v3.1.0 2016-05-30
* `NewClientWithSender(Sender, string) (Statter, error)` method added to
enable building a Client from a prefix and an already created Sender.
* Add stat recording sender in submodule statsdtest (GH-32).
* Add an example helper stat validation function.
* Change the way scope joins are done (GH-26).
* Reorder some structs to avoid middle padding.
## 3.0.3 2016-02-18
* make sampler function tunable (GH-24)
## 3.0.2 2016-01-13
* reduce memory allocations
* improve performance of buffered clients
## 3.0.1 2016-01-01
* documentation typo fixes
* fix possible race condition with `buffered_sender` send/close.
## 3.0.0 2015-12-04
* add substatter support
## 2.0.2 2015-10-16
* remove trailing newline in buffered sends to avoid etsy statsd log messages
* minor internal code reorganization for clarity (no api changes)
## 2.0.1 2015-07-12
* Add Set and SetInt funcs to support Sets
* Properly flush BufferedSender on close (bugfix)
* Add TimingDuration with support for sub-millisecond timing
* fewer allocations, better performance of BufferedClient
## 2.0.0 2015-03-19
* BufferedClient - send multiple stats at once
* clean up godocs
* clean up interfaces -- BREAKING CHANGE: for users who previously defined
types as *Client instead of the Statter interface type.
## 1.0.1 2015-03-19
* BufferedClient - send multiple stats at once
## 1.0.0 2015-02-04
* tag a version as fix for GH-8

View File

@@ -1,19 +0,0 @@
Copyright (c) 2012-2016 Eli Janssen
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,68 +0,0 @@
go-statsd-client
================
[![Build Status](https://travis-ci.org/cactus/go-statsd-client.png?branch=master)](https://travis-ci.org/cactus/go-statsd-client)
[![GoDoc](https://godoc.org/github.com/cactus/go-statsd-client/statsd?status.png)](https://godoc.org/github.com/cactus/go-statsd-client/statsd)
[![Go Report Card](https://goreportcard.com/badge/cactus/go-statsd-client)](https://goreportcard.com/report/cactus/go-statsd-client)
## About
A [StatsD][1] client for Go.
## Docs
Viewable online at [godoc.org][2].
## Example
``` go
import (
"log"
"github.com/cactus/go-statsd-client/statsd"
)
func main() {
// first create a client
// The basic client sends one stat per packet (for compatibility).
client, err := statsd.NewClient("127.0.0.1:8125", "test-client")
// A buffered client, which sends multiple stats in one packet, is
// recommended when your server supports it (better performance).
// client, err := statsd.NewBufferedClient("127.0.0.1:8125", "test-client", 300*time.Millisecond, 0)
// handle any errors
if err != nil {
log.Fatal(err)
}
// make sure to clean up
defer client.Close()
// Send a stat
client.Inc("stat1", 42, 1.0)
}
```
See [docs][2] for more info. There is also some simple example code in the
`test-client` directory.
## Contributors
See [here][4].
## Alternative Implementations
See the [statsd wiki][5] for some additional client implementations
(scroll down to the Go section).
## License
Released under the [MIT license][3]. See `LICENSE.md` file for details.
[1]: https://github.com/etsy/statsd
[2]: http://godoc.org/github.com/cactus/go-statsd-client/statsd
[3]: http://www.opensource.org/licenses/mit-license.php
[4]: https://github.com/cactus/go-statsd-client/graphs/contributors
[5]: https://github.com/etsy/statsd/wiki#client-implementations

View File

@@ -1,98 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"testing"
"time"
)
func BenchmarkBufferedClientInc(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0)
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.Inc("benchinc", 1, 1)
}
})
}
func BenchmarkBufferedClientIncSample(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0)
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.Inc("benchinc", 1, 0.3)
}
})
}
func BenchmarkBufferedClientSetInt(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0)
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.SetInt("setint", 1, 1)
}
})
}
func BenchmarkBufferedClientSetIntSample(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 0)
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.SetInt("setint", 1, 0.3)
}
})
}

View File

@@ -1,97 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"testing"
)
func BenchmarkClientInc(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewClient(l.LocalAddr().String(), "test")
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.Inc("benchinc", 1, 1)
}
})
}
func BenchmarkClientIncSample(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewClient(l.LocalAddr().String(), "test")
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.Inc("benchinc", 1, 0.3)
}
})
}
func BenchmarkClientSetInt(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewClient(l.LocalAddr().String(), "test")
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.SetInt("setint", 1, 1)
}
})
}
func BenchmarkClientSetIntSample(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
c, err := NewClient(l.LocalAddr().String(), "test")
if err != nil {
b.Fatal(err)
}
defer c.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
c.SetInt("setint", 1, 0.3)
}
})
}

View File

@@ -1,102 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"testing"
"time"
)
func BenchmarkSenderSmall(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
s, err := NewSimpleSender(l.LocalAddr().String())
if err != nil {
b.Fatal(err)
}
defer s.Close()
data := []byte("test.gauge:1|g\n")
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
s.Send(data)
}
})
}
func BenchmarkSenderLarge(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
s, err := NewSimpleSender(l.LocalAddr().String())
if err != nil {
b.Fatal(err)
}
defer s.Close()
data := bytes.Repeat([]byte("test.gauge:1|g\n"), 50)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
s.Send(data)
}
})
}
func BenchmarkBufferedSenderSmall(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
s, err := NewBufferedSender(l.LocalAddr().String(), 300*time.Millisecond, 1432)
if err != nil {
b.Fatal(err)
}
defer s.Close()
data := []byte("test.gauge:1|g\n")
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
s.Send(data)
}
})
}
func BenchmarkBufferedSenderLarge(b *testing.B) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer l.Close()
s, err := NewBufferedSender(l.LocalAddr().String(), 300*time.Millisecond, 1432)
if err != nil {
b.Fatal(err)
}
defer s.Close()
data := bytes.Repeat([]byte("test.gauge:1|g\n"), 50)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
//i := 0; i < b.N; i++ {
s.Send(data)
}
})
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"sync"
)
type bufferPool struct {
*sync.Pool
}
func newBufferPool() *bufferPool {
return &bufferPool{
&sync.Pool{New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 1700))
}},
}
}
func (bp *bufferPool) Get() *bytes.Buffer {
return (bp.Pool.Get()).(*bytes.Buffer)
}
func (bp *bufferPool) Put(b *bytes.Buffer) {
b.Truncate(0)
bp.Pool.Put(b)
}

View File

@@ -1,330 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
)
var bufPool = newBufferPool()
// The StatSender interface wraps all the statsd metric methods
type StatSender interface {
Inc(string, int64, float32) error
Dec(string, int64, float32) error
Gauge(string, int64, float32) error
GaugeDelta(string, int64, float32) error
Timing(string, int64, float32) error
TimingDuration(string, time.Duration, float32) error
Set(string, string, float32) error
SetInt(string, int64, float32) error
Raw(string, string, float32) error
}
// The Statter interface defines the behavior of a stat client
type Statter interface {
StatSender
NewSubStatter(string) SubStatter
SetPrefix(string)
Close() error
}
// The SubStatter interface defines the behavior of a stat child/subclient
type SubStatter interface {
StatSender
SetSamplerFunc(SamplerFunc)
NewSubStatter(string) SubStatter
}
// The SamplerFunc type defines a function that can serve
// as a Client sampler function.
type SamplerFunc func(float32) bool
// DefaultSampler is the default rate sampler function
func DefaultSampler(rate float32) bool {
if rate < 1 {
if rand.Float32() < rate {
return true
}
return false
}
return true
}
// A Client is a statsd client.
type Client struct {
// prefix for statsd name
prefix string
// packet sender
sender Sender
// sampler method
sampler SamplerFunc
}
// Close closes the connection and cleans up.
func (s *Client) Close() error {
if s == nil {
return nil
}
err := s.sender.Close()
return err
}
// Inc increments a statsd count type.
// stat is a string name for the metric.
// value is the integer value
// rate is the sample rate (0.0 to 1.0)
func (s *Client) Inc(stat string, value int64, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", value, "|c", rate)
}
// Dec decrements a statsd count type.
// stat is a string name for the metric.
// value is the integer value.
// rate is the sample rate (0.0 to 1.0).
func (s *Client) Dec(stat string, value int64, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", -value, "|c", rate)
}
// Gauge submits/updates a statsd gauge type.
// stat is a string name for the metric.
// value is the integer value.
// rate is the sample rate (0.0 to 1.0).
func (s *Client) Gauge(stat string, value int64, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", value, "|g", rate)
}
// GaugeDelta submits a delta to a statsd gauge.
// stat is the string name for the metric.
// value is the (positive or negative) change.
// rate is the sample rate (0.0 to 1.0).
func (s *Client) GaugeDelta(stat string, value int64, rate float32) error {
if !s.includeStat(rate) {
return nil
}
// if negative, the submit formatter will prefix with a - already
// so only special case the positive value
if value >= 0 {
return s.submit(stat, "+", value, "|g", rate)
}
return s.submit(stat, "", value, "|g", rate)
}
// Timing submits a statsd timing type.
// stat is a string name for the metric.
// delta is the time duration value in milliseconds
// rate is the sample rate (0.0 to 1.0).
func (s *Client) Timing(stat string, delta int64, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", delta, "|ms", rate)
}
// TimingDuration submits a statsd timing type.
// stat is a string name for the metric.
// delta is the timing value as time.Duration
// rate is the sample rate (0.0 to 1.0).
func (s *Client) TimingDuration(stat string, delta time.Duration, rate float32) error {
if !s.includeStat(rate) {
return nil
}
ms := float64(delta) / float64(time.Millisecond)
return s.submit(stat, "", ms, "|ms", rate)
}
// Set submits a stats set type
// stat is a string name for the metric.
// value is the string value
// rate is the sample rate (0.0 to 1.0).
func (s *Client) Set(stat string, value string, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", value, "|s", rate)
}
// SetInt submits a number as a stats set type.
// stat is a string name for the metric.
// value is the integer value
// rate is the sample rate (0.0 to 1.0).
func (s *Client) SetInt(stat string, value int64, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", value, "|s", rate)
}
// Raw submits a preformatted value.
// stat is the string name for the metric.
// value is a preformatted "raw" value string.
// rate is the sample rate (0.0 to 1.0).
func (s *Client) Raw(stat string, value string, rate float32) error {
if !s.includeStat(rate) {
return nil
}
return s.submit(stat, "", value, "", rate)
}
// SetSamplerFunc sets a sampler function to something other than the default
// sampler is a function that determines whether the metric is
// to be accepted, or discarded.
// An example use case is for submitted pre-sampled metrics.
func (s *Client) SetSamplerFunc(sampler SamplerFunc) {
s.sampler = sampler
}
// submit an already sampled raw stat
func (s *Client) submit(stat, vprefix string, value interface{}, suffix string, rate float32) error {
data := bufPool.Get()
defer bufPool.Put(data)
if s.prefix != "" {
data.WriteString(s.prefix)
data.WriteString(".")
}
data.WriteString(stat)
data.WriteString(":")
if vprefix != "" {
data.WriteString(vprefix)
}
// sadly, no way to jam this back into the bytes.Buffer without
// doing a few allocations... avoiding those is the whole point here...
// so from here on out just use it as a raw []byte
b := data.Bytes()
switch v := value.(type) {
case string:
b = append(b, v...)
case int64:
b = strconv.AppendInt(b, v, 10)
case float64:
b = strconv.AppendFloat(b, v, 'f', -1, 64)
default:
return fmt.Errorf("No matching type format")
}
if suffix != "" {
b = append(b, suffix...)
}
if rate < 1 {
b = append(b, "|@"...)
b = strconv.AppendFloat(b, float64(rate), 'f', 6, 32)
}
_, err := s.sender.Send(b)
return err
}
// check for nil client, and perform sampling calculation
func (s *Client) includeStat(rate float32) bool {
if s == nil {
return false
}
// test for nil in case someone builds their own
// client without calling new (result is nil sampler)
if s.sampler != nil {
return s.sampler(rate)
}
return DefaultSampler(rate)
}
// SetPrefix sets/updates the statsd client prefix.
// Note: Does not change the prefix of any SubStatters.
func (s *Client) SetPrefix(prefix string) {
if s == nil {
return
}
s.prefix = prefix
}
// NewSubStatter returns a SubStatter with appended prefix
func (s *Client) NewSubStatter(prefix string) SubStatter {
var c *Client
if s != nil {
c = &Client{
prefix: joinPathComp(s.prefix, prefix),
sender: s.sender,
sampler: s.sampler,
}
}
return c
}
// NewClient returns a pointer to a new Client, and an error.
//
// addr is a string of the format "hostname:port", and must be parsable by
// net.ResolveUDPAddr.
//
// prefix is the statsd client prefix. Can be "" if no prefix is desired.
func NewClient(addr, prefix string) (Statter, error) {
sender, err := NewSimpleSender(addr)
if err != nil {
return nil, err
}
return &Client{prefix: prefix, sender: sender}, nil
}
// NewClientWithSender returns a pointer to a new Client and an error.
//
// sender is an instance of a statsd.Sender interface and may not be nil
//
// prefix is the stastd client prefix. Can be "" if no prefix is desired.
func NewClientWithSender(sender Sender, prefix string) (Statter, error) {
if sender == nil {
return nil, fmt.Errorf("Client sender may not be nil")
}
return &Client{prefix: prefix, sender: sender}, nil
}
// joinPathComp is a helper that ensures we combine path components with a dot
// when it's appropriate to do so; prefix is the existing prefix and suffix is
// the new component being added.
//
// It returns the joined prefix.
func joinPathComp(prefix, suffix string) string {
suffix = strings.TrimLeft(suffix, ".")
if prefix != "" && suffix != "" {
return prefix + "." + suffix
}
return prefix + suffix
}
// Dial is a compatibility alias for NewClient
var Dial = NewClient
// New is a compatibility alias for NewClient
var New = NewClient

View File

@@ -1,48 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import "time"
// NewBufferedClient returns a new BufferedClient
//
// addr is a string of the format "hostname:port", and must be parsable by
// net.ResolveUDPAddr.
//
// prefix is the statsd client prefix. Can be "" if no prefix is desired.
//
// flushInterval is a time.Duration, and specifies the maximum interval for
// packet sending. Note that if you send lots of metrics, you will send more
// often. This is just a maximal threshold.
//
// If flushInterval is 0ms, defaults to 300ms.
//
// flushBytes specifies the maximum udp packet size you wish to send. If adding
// a metric would result in a larger packet than flushBytes, the packet will
// first be send, then the new data will be added to the next packet.
//
// If flushBytes is 0, defaults to 1432 bytes, which is considered safe
// for local traffic. If sending over the public internet, 512 bytes is
// the recommended value.
func NewBufferedClient(addr, prefix string, flushInterval time.Duration, flushBytes int) (Statter, error) {
if flushBytes <= 0 {
// https://github.com/etsy/statsd/blob/master/docs/metric_types.md#multi-metric-packets
flushBytes = 1432
}
if flushInterval <= time.Duration(0) {
flushInterval = 300 * time.Millisecond
}
sender, err := NewBufferedSender(addr, flushInterval, flushBytes)
if err != nil {
return nil, err
}
client := &Client{
prefix: prefix,
sender: sender,
}
return client, nil
}

View File

@@ -1,203 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"fmt"
"log"
"reflect"
"strings"
"testing"
"time"
)
func TestBufferedClientFlushSize(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdPacketTests {
// set flush length to the size of the expected output packet
// so we can ensure a flush happens right away.
// set flush time sufficiently high so that it never matters for this
// test
c, err := NewBufferedClient(l.LocalAddr().String(), tt.Prefix, 10*time.Second, len(tt.Expected)+1)
if err != nil {
c.Close()
t.Fatal(err)
}
method := reflect.ValueOf(c).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
c.Close()
t.Fatal(errInter.(error))
}
data := make([]byte, len(tt.Expected)+16)
_, _, err = l.ReadFrom(data)
if err != nil {
c.Close()
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00\n")
if bytes.Equal(data, []byte(tt.Expected)) != true {
t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected)
}
c.Close()
}
}
func TestBufferedClientFlushTime(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdPacketTests {
// set flush length to the size of the expected output packet
// so we can ensure a flush happens right away.
// set flush time sufficiently high so that it never matters for this
// test
c, err := NewBufferedClient(l.LocalAddr().String(), tt.Prefix, 1*time.Microsecond, 1024)
if err != nil {
c.Close()
t.Fatal(err)
}
method := reflect.ValueOf(c).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
c.Close()
t.Fatal(errInter.(error))
}
time.Sleep(1 * time.Millisecond)
data := make([]byte, len(tt.Expected)+16)
_, _, err = l.ReadFrom(data)
if err != nil {
c.Close()
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00\n")
if bytes.Equal(data, []byte(tt.Expected)) != true {
t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected)
}
c.Close()
}
}
func TestBufferedClientBigPacket(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
c, err := NewBufferedClient(l.LocalAddr().String(), "test", 10*time.Millisecond, 1024)
if err != nil {
t.Fatal(err)
}
defer c.Close()
for _, tt := range statsdPacketTests {
if tt.Prefix != "test" {
continue
}
method := reflect.ValueOf(c).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
}
expected := ""
for _, tt := range statsdPacketTests {
if tt.Prefix != "test" {
continue
}
expected = expected + tt.Expected + "\n"
}
expected = strings.TrimSuffix(expected, "\n")
time.Sleep(12 * time.Millisecond)
data := make([]byte, 1024)
_, _, err = l.ReadFrom(data)
if err != nil {
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00")
if bytes.Equal(data, []byte(expected)) != true {
t.Fatalf("got '%s' expected '%s'", data, expected)
}
}
func TestFlushOnClose(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
c, err := NewBufferedClient(l.LocalAddr().String(), "test", 1*time.Second, 1024)
if err != nil {
t.Fatal(err)
}
c.Inc("count", int64(1), 1.0)
c.Close()
expected := "test.count:1|c"
data := make([]byte, 1024)
_, _, err = l.ReadFrom(data)
if err != nil {
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00")
if bytes.Equal(data, []byte(expected)) != true {
fmt.Println(data)
fmt.Println([]byte(expected))
t.Fatalf("got '%s' expected '%s'", data, expected)
}
}
func ExampleClient_buffered() {
// first create a client
client, err := NewBufferedClient("127.0.0.1:8125", "test-client", 10*time.Millisecond, 0)
// handle any errors
if err != nil {
log.Fatal(err)
}
// make sure to clean up
defer client.Close()
// Send a stat
err = client.Inc("stat1", 42, 1.0)
// handle any errors
if err != nil {
log.Printf("Error sending metric: %+v", err)
}
}

View File

@@ -1,111 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import "time"
// A NoopClient is a client that does nothing.
type NoopClient struct{}
// Close closes the connection and cleans up.
func (s *NoopClient) Close() error {
return nil
}
// Inc increments a statsd count type.
// stat is a string name for the metric.
// value is the integer value
// rate is the sample rate (0.0 to 1.0)
func (s *NoopClient) Inc(stat string, value int64, rate float32) error {
return nil
}
// Dec decrements a statsd count type.
// stat is a string name for the metric.
// value is the integer value.
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) Dec(stat string, value int64, rate float32) error {
return nil
}
// Gauge submits/Updates a statsd gauge type.
// stat is a string name for the metric.
// value is the integer value.
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) Gauge(stat string, value int64, rate float32) error {
return nil
}
// GaugeDelta submits a delta to a statsd gauge.
// stat is the string name for the metric.
// value is the (positive or negative) change.
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) GaugeDelta(stat string, value int64, rate float32) error {
return nil
}
// Timing submits a statsd timing type.
// stat is a string name for the metric.
// delta is the time duration value in milliseconds
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) Timing(stat string, delta int64, rate float32) error {
return nil
}
// TimingDuration submits a statsd timing type.
// stat is a string name for the metric.
// delta is the timing value as time.Duration
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) TimingDuration(stat string, delta time.Duration, rate float32) error {
return nil
}
// Set submits a stats set type.
// stat is a string name for the metric.
// value is the string value
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) Set(stat string, value string, rate float32) error {
return nil
}
// SetInt submits a number as a stats set type.
// convenience method for Set with number.
// stat is a string name for the metric.
// value is the integer value
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) SetInt(stat string, value int64, rate float32) error {
return nil
}
// Raw formats the statsd event data, handles sampling, prepares it,
// and sends it to the server.
// stat is the string name for the metric.
// value is the preformatted "raw" value string.
// rate is the sample rate (0.0 to 1.0).
func (s *NoopClient) Raw(stat string, value string, rate float32) error {
return nil
}
// SetPrefix sets/updates the statsd client prefix
func (s *NoopClient) SetPrefix(prefix string) {}
// NewSubStatter returns a SubStatter with appended prefix
func (s *NoopClient) NewSubStatter(prefix string) SubStatter {
return &NoopClient{}
}
// SetSamplerFunc sets the sampler function
func (s *NoopClient) SetSamplerFunc(sampler SamplerFunc) {}
// NewNoopClient returns a pointer to a new NoopClient, and an error (always
// nil, just supplied to support api convention).
// Use variadic arguments to support identical format as NewClient, or a more
// conventional no argument form.
func NewNoopClient(a ...interface{}) (Statter, error) {
return &NoopClient{}, nil
}
// NewNoop is a compatibility alias for NewNoopClient
var NewNoop = NewNoopClient

View File

@@ -1,292 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"log"
"reflect"
"strings"
"testing"
"time"
)
var statsdSubStatterPacketTests = []struct {
Prefix string
SubPrefix string
Method string
Stat string
Value interface{}
Rate float32
Expected string
}{
{"test", "sub", "Gauge", "gauge", int64(1), 1.0, "test.sub.gauge:1|g"},
{"test", "sub", "Inc", "count", int64(1), 0.999999, "test.sub.count:1|c|@0.999999"},
{"test", "sub", "Inc", "count", int64(1), 1.0, "test.sub.count:1|c"},
{"test", "sub", "Dec", "count", int64(1), 1.0, "test.sub.count:-1|c"},
{"test", "sub", "Timing", "timing", int64(1), 1.0, "test.sub.timing:1|ms"},
{"test", "sub", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "test.sub.timing:1.5|ms"},
{"test", "sub", "TimingDuration", "timing", 3 * time.Microsecond, 1.0, "test.sub.timing:0.003|ms"},
{"test", "sub", "Set", "strset", "pickle", 1.0, "test.sub.strset:pickle|s"},
{"test", "sub", "SetInt", "intset", int64(1), 1.0, "test.sub.intset:1|s"},
{"test", "sub", "GaugeDelta", "gauge", int64(1), 1.0, "test.sub.gauge:+1|g"},
{"test", "sub", "GaugeDelta", "gauge", int64(-1), 1.0, "test.sub.gauge:-1|g"},
// empty sub prefix -- note: not used in subsub tests
{"test", "", "Inc", "count", int64(1), 1.0, "test.count:1|c"},
// empty base prefix
{"", "sub", "Inc", "count", int64(1), 1.0, "sub.count:1|c"},
}
func TestSubStatterClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdSubStatterPacketTests {
c, err := NewClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
s := c.NewSubStatter(tt.SubPrefix)
method := reflect.ValueOf(s).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
_, _, err = l.ReadFrom(data)
if err != nil {
c.Close()
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00")
if bytes.Equal(data, []byte(tt.Expected)) != true {
c.Close()
t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected)
}
c.Close()
}
}
func TestMultSubStatterClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdSubStatterPacketTests {
// ignore empty sub test for this, as there is nothing to regex sub
if tt.SubPrefix == "" {
continue
}
c, err := NewClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
s1 := c.NewSubStatter("sub1")
s2 := c.NewSubStatter("sub2")
responses := [][]byte{}
for _, s := range []SubStatter{s1, s2} {
method := reflect.ValueOf(s).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
_, _, err = l.ReadFrom(data)
if err != nil {
c.Close()
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00")
responses = append(responses, data)
}
expected := strings.Replace(tt.Expected, "sub.", "sub1.", -1)
if bytes.Equal(responses[0], []byte(expected)) != true {
c.Close()
t.Fatalf("%s got '%s' expected '%s'",
tt.Method, responses[0], tt.Expected)
}
expected = strings.Replace(tt.Expected, "sub.", "sub2.", -1)
if bytes.Equal(responses[1], []byte(expected)) != true {
c.Close()
t.Fatalf("%s got '%s' expected '%s'",
tt.Method, responses[1], tt.Expected)
}
c.Close()
}
}
func TestSubSubStatterClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdSubStatterPacketTests {
// ignore empty sub test for this, as there is nothing to regex sub
if tt.SubPrefix == "" {
continue
}
c, err := NewClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
s := c.NewSubStatter(tt.SubPrefix).NewSubStatter("sub2")
method := reflect.ValueOf(s).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
_, _, err = l.ReadFrom(data)
if err != nil {
c.Close()
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00")
expected := strings.Replace(tt.Expected, "sub.", "sub.sub2.", -1)
if bytes.Equal(data, []byte(expected)) != true {
c.Close()
t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected)
}
c.Close()
}
}
func TestSubStatterClosedClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdSubStatterPacketTests {
c, err := NewClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
c.Close()
s := c.NewSubStatter(tt.SubPrefix)
method := reflect.ValueOf(s).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter == nil {
t.Fatal("Expected error but got none")
}
}
}
func TestNilSubStatterClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdSubStatterPacketTests {
var c *Client
s := c.NewSubStatter(tt.SubPrefix)
method := reflect.ValueOf(s).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
n, _, err := l.ReadFrom(data)
// this is expected to error, since there should
// be no udp data sent, so the read will time out
if err == nil || n != 0 {
c.Close()
t.Fatal(err)
}
c.Close()
}
}
func TestNoopSubStatterClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdSubStatterPacketTests {
c, err := NewNoopClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
s := c.NewSubStatter(tt.SubPrefix)
method := reflect.ValueOf(s).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
n, _, err := l.ReadFrom(data)
// this is expected to error, since there should
// be no udp data sent, so the read will time out
if err == nil || n != 0 {
c.Close()
t.Fatal(err)
}
c.Close()
}
}
func ExampleClient_substatter() {
// first create a client
client, err := NewClient("127.0.0.1:8125", "test-client")
// handle any errors
if err != nil {
log.Fatal(err)
}
// make sure to clean up
defer client.Close()
// create a substatter
subclient := client.NewSubStatter("sub")
// send a stat
err = subclient.Inc("stat1", 42, 1.0)
// handle any errors
if err != nil {
log.Printf("Error sending metric: %+v", err)
}
}

View File

@@ -1,201 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"log"
"net"
"reflect"
"testing"
"time"
)
var statsdPacketTests = []struct {
Prefix string
Method string
Stat string
Value interface{}
Rate float32
Expected string
}{
{"test", "Gauge", "gauge", int64(1), 1.0, "test.gauge:1|g"},
{"test", "Inc", "count", int64(1), 0.999999, "test.count:1|c|@0.999999"},
{"test", "Inc", "count", int64(1), 1.0, "test.count:1|c"},
{"test", "Dec", "count", int64(1), 1.0, "test.count:-1|c"},
{"test", "Timing", "timing", int64(1), 1.0, "test.timing:1|ms"},
{"test", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "test.timing:1.5|ms"},
{"test", "TimingDuration", "timing", 3 * time.Microsecond, 1.0, "test.timing:0.003|ms"},
{"test", "Set", "strset", "pickle", 1.0, "test.strset:pickle|s"},
{"test", "SetInt", "intset", int64(1), 1.0, "test.intset:1|s"},
{"test", "GaugeDelta", "gauge", int64(1), 1.0, "test.gauge:+1|g"},
{"test", "GaugeDelta", "gauge", int64(-1), 1.0, "test.gauge:-1|g"},
{"", "Gauge", "gauge", int64(1), 1.0, "gauge:1|g"},
{"", "Inc", "count", int64(1), 0.999999, "count:1|c|@0.999999"},
{"", "Inc", "count", int64(1), 1.0, "count:1|c"},
{"", "Dec", "count", int64(1), 1.0, "count:-1|c"},
{"", "Timing", "timing", int64(1), 1.0, "timing:1|ms"},
{"", "TimingDuration", "timing", 1500 * time.Microsecond, 1.0, "timing:1.5|ms"},
{"", "Set", "strset", "pickle", 1.0, "strset:pickle|s"},
{"", "SetInt", "intset", int64(1), 1.0, "intset:1|s"},
{"", "GaugeDelta", "gauge", int64(1), 1.0, "gauge:+1|g"},
{"", "GaugeDelta", "gauge", int64(-1), 1.0, "gauge:-1|g"},
}
func TestClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdPacketTests {
c, err := NewClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
method := reflect.ValueOf(c).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
_, _, err = l.ReadFrom(data)
if err != nil {
c.Close()
t.Fatal(err)
}
data = bytes.TrimRight(data, "\x00")
if bytes.Equal(data, []byte(tt.Expected)) != true {
c.Close()
t.Fatalf("%s got '%s' expected '%s'", tt.Method, data, tt.Expected)
}
c.Close()
}
}
func TestNilClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdPacketTests {
var c *Client
method := reflect.ValueOf(c).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
n, _, err := l.ReadFrom(data)
// this is expected to error, since there should
// be no udp data sent, so the read will time out
if err == nil || n != 0 {
c.Close()
t.Fatal(err)
}
c.Close()
}
}
func TestNoopClient(t *testing.T) {
l, err := newUDPListener("127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
for _, tt := range statsdPacketTests {
c, err := NewNoopClient(l.LocalAddr().String(), tt.Prefix)
if err != nil {
t.Fatal(err)
}
method := reflect.ValueOf(c).MethodByName(tt.Method)
e := method.Call([]reflect.Value{
reflect.ValueOf(tt.Stat),
reflect.ValueOf(tt.Value),
reflect.ValueOf(tt.Rate)})[0]
errInter := e.Interface()
if errInter != nil {
t.Fatal(errInter.(error))
}
data := make([]byte, 128)
n, _, err := l.ReadFrom(data)
// this is expected to error, since there should
// be no udp data sent, so the read will time out
if err == nil || n != 0 {
c.Close()
t.Fatal(err)
}
c.Close()
}
}
func newUDPListener(addr string) (*net.UDPConn, error) {
l, err := net.ListenPacket("udp", addr)
if err != nil {
return nil, err
}
l.SetDeadline(time.Now().Add(100 * time.Millisecond))
l.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
l.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
return l.(*net.UDPConn), nil
}
func ExampleClient() {
// first create a client
client, err := NewClient("127.0.0.1:8125", "test-client")
// handle any errors
if err != nil {
log.Fatal(err)
}
// make sure to clean up
defer client.Close()
// Send a stat
err = client.Inc("stat1", 42, 1.0)
// handle any errors
if err != nil {
log.Printf("Error sending metric: %+v", err)
}
}
func ExampleClient_noop() {
// use interface so we can sub noop client if needed
var client Statter
var err error
// first try to create a real client
client, err = NewClient("not-resolvable:8125", "test-client")
// Let us say real client creation fails, but you don't care enough about
// stats that you don't want your program to run. Just log an error and
// make a NoopClient instead
if err != nil {
log.Println("Remote endpoint did not resolve. Disabling stats", err)
client, err = NewNoopClient()
}
// make sure to clean up
defer client.Close()
// Send a stat
err = client.Inc("stat1", 42, 1.0)
// handle any errors
if err != nil {
log.Printf("Error sending metric: %+v", err)
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
/*
Package statsd provides a StatsD client implementation that is safe for
concurrent use by multiple goroutines and for efficiency can be created and
reused.
Example usage:
// first create a client
client, err := statsd.NewClient("127.0.0.1:8125", "test-client")
// handle any errors
if err != nil {
log.Fatal(err)
}
// make sure to clean up
defer client.Close()
// Send a stat
err = client.Inc("stat1", 42, 1.0)
// handle any errors
if err != nil {
log.Printf("Error sending metric: %+v", err)
}
*/
package statsd

View File

@@ -1,69 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"errors"
"net"
)
// The Sender interface wraps a Send and Close
type Sender interface {
Send(data []byte) (int, error)
Close() error
}
// SimpleSender provides a socket send interface.
type SimpleSender struct {
// underlying connection
c net.PacketConn
// resolved udp address
ra *net.UDPAddr
}
// Send sends the data to the server endpoint.
func (s *SimpleSender) Send(data []byte) (int, error) {
// no need for locking here, as the underlying fdNet
// already serialized writes
n, err := s.c.(*net.UDPConn).WriteToUDP(data, s.ra)
if err != nil {
return 0, err
}
if n == 0 {
return n, errors.New("Wrote no bytes")
}
return n, nil
}
// Close closes the SimpleSender
func (s *SimpleSender) Close() error {
err := s.c.Close()
return err
}
// NewSimpleSender returns a new SimpleSender for sending to the supplied
// addresss.
//
// addr is a string of the format "hostname:port", and must be parsable by
// net.ResolveUDPAddr.
func NewSimpleSender(addr string) (Sender, error) {
c, err := net.ListenPacket("udp", ":0")
if err != nil {
return nil, err
}
ra, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
c.Close()
return nil, err
}
sender := &SimpleSender{
c: c,
ra: ra,
}
return sender, nil
}

View File

@@ -1,175 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"fmt"
"sync"
"time"
)
var senderPool = newBufferPool()
// BufferedSender provides a buffered statsd udp, sending multiple
// metrics, where possible.
type BufferedSender struct {
sender Sender
flushBytes int
flushInterval time.Duration
// buffers
bufmx sync.Mutex
buffer *bytes.Buffer
bufs chan *bytes.Buffer
// lifecycle
runmx sync.RWMutex
shutdown chan chan error
running bool
}
// Send bytes.
func (s *BufferedSender) Send(data []byte) (int, error) {
s.runmx.RLock()
if !s.running {
s.runmx.RUnlock()
return 0, fmt.Errorf("BufferedSender is not running")
}
s.withBufferLock(func() {
blen := s.buffer.Len()
if blen > 0 && blen+len(data)+1 >= s.flushBytes {
s.swapnqueue()
}
s.buffer.Write(data)
s.buffer.WriteByte('\n')
if s.buffer.Len() >= s.flushBytes {
s.swapnqueue()
}
})
s.runmx.RUnlock()
return len(data), nil
}
// Close Buffered Sender
func (s *BufferedSender) Close() error {
// since we are running, write lock during cleanup
s.runmx.Lock()
defer s.runmx.Unlock()
if !s.running {
return nil
}
errChan := make(chan error)
s.running = false
s.shutdown <- errChan
return <-errChan
}
// Start Buffered Sender
// Begins ticker and read loop
func (s *BufferedSender) Start() {
// write lock to start running
s.runmx.Lock()
defer s.runmx.Unlock()
if s.running {
return
}
s.running = true
s.bufs = make(chan *bytes.Buffer, 32)
go s.run()
}
func (s *BufferedSender) withBufferLock(fn func()) {
s.bufmx.Lock()
fn()
s.bufmx.Unlock()
}
func (s *BufferedSender) swapnqueue() {
if s.buffer.Len() == 0 {
return
}
ob := s.buffer
nb := senderPool.Get()
s.buffer = nb
s.bufs <- ob
}
func (s *BufferedSender) run() {
ticker := time.NewTicker(s.flushInterval)
defer ticker.Stop()
doneChan := make(chan bool)
go func() {
for buf := range s.bufs {
s.flush(buf)
senderPool.Put(buf)
}
doneChan <- true
}()
for {
select {
case <-ticker.C:
s.withBufferLock(func() {
s.swapnqueue()
})
case errChan := <-s.shutdown:
s.withBufferLock(func() {
s.swapnqueue()
})
close(s.bufs)
<-doneChan
errChan <- s.sender.Close()
return
}
}
}
// send to remove endpoint and truncate buffer
func (s *BufferedSender) flush(b *bytes.Buffer) (int, error) {
bb := b.Bytes()
bbl := len(bb)
if bb[bbl-1] == '\n' {
bb = bb[:bbl-1]
}
//n, err := s.sender.Send(bytes.TrimSuffix(b.Bytes(), []byte("\n")))
n, err := s.sender.Send(bb)
b.Truncate(0) // clear the buffer
return n, err
}
// NewBufferedSender returns a new BufferedSender
//
// addr is a string of the format "hostname:port", and must be parsable by
// net.ResolveUDPAddr.
//
// flushInterval is a time.Duration, and specifies the maximum interval for
// packet sending. Note that if you send lots of metrics, you will send more
// often. This is just a maximal threshold.
//
// flushBytes specifies the maximum udp packet size you wish to send. If adding
// a metric would result in a larger packet than flushBytes, the packet will
// first be send, then the new data will be added to the next packet.
func NewBufferedSender(addr string, flushInterval time.Duration, flushBytes int) (Sender, error) {
simpleSender, err := NewSimpleSender(addr)
if err != nil {
return nil, err
}
sender := &BufferedSender{
flushBytes: flushBytes,
flushInterval: flushInterval,
sender: simpleSender,
buffer: senderPool.Get(),
shutdown: make(chan chan error),
}
sender.Start()
return sender, nil
}

View File

@@ -1,116 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"bytes"
"testing"
"time"
)
type mockSender struct {
closeCallCount int
}
func (m *mockSender) Send(data []byte) (int, error) {
return 0, nil
}
func (m *mockSender) Close() error {
m.closeCallCount++
return nil
}
func TestClose(t *testing.T) {
mockSender := &mockSender{}
sender := &BufferedSender{
flushBytes: 512,
flushInterval: 1 * time.Second,
sender: mockSender,
buffer: bytes.NewBuffer(make([]byte, 0, 512)),
shutdown: make(chan chan error),
}
sender.Close()
if mockSender.closeCallCount != 0 {
t.Fatalf("expected close to have been called zero times, but got %d", mockSender.closeCallCount)
}
sender.Start()
if !sender.running {
t.Fatal("sender failed to start")
}
sender.Close()
if mockSender.closeCallCount != 1 {
t.Fatalf("expected close to have been called once, but got %d", mockSender.closeCallCount)
}
}
func TestCloseConcurrent(t *testing.T) {
mockSender := &mockSender{}
sender := &BufferedSender{
flushBytes: 512,
flushInterval: 1 * time.Second,
sender: mockSender,
buffer: bytes.NewBuffer(make([]byte, 0, 512)),
shutdown: make(chan chan error),
}
sender.Start()
const N = 10
c := make(chan struct{}, N)
for i := 0; i < N; i++ {
go func() {
sender.Close()
c <- struct{}{}
}()
}
for i := 0; i < N; i++ {
<-c
}
if mockSender.closeCallCount != 1 {
t.Errorf("expected close to have been called once, but got %d", mockSender.closeCallCount)
}
}
func TestCloseDuringSendConcurrent(t *testing.T) {
mockSender := &mockSender{}
sender := &BufferedSender{
flushBytes: 512,
flushInterval: 1 * time.Second,
sender: mockSender,
buffer: bytes.NewBuffer(make([]byte, 0, 512)),
shutdown: make(chan chan error),
}
sender.Start()
const N = 10
c := make(chan struct{}, N)
for i := 0; i < N; i++ {
go func() {
for {
_, err := sender.Send([]byte("stat:1|c"))
if err != nil {
c <- struct{}{}
return
}
}
}()
}
// senders should error out now
// we should not receive any panics
sender.Close()
for i := 0; i < N; i++ {
<-c
}
if mockSender.closeCallCount != 1 {
t.Errorf("expected close to have been called once, but got %d", mockSender.closeCallCount)
}
}

View File

@@ -1,81 +0,0 @@
package statsdtest
import (
"errors"
"sync"
)
// RecordingSender implements statsd.Sender but parses individual Stats into a
// buffer that can be later inspected instead of sending to some server. It
// should constructed with NewRecordingSender().
type RecordingSender struct {
m sync.Mutex
buffer Stats
closed bool
}
// NewRecordingSender creates a new RecordingSender for use by a statsd.Client.
func NewRecordingSender() *RecordingSender {
rs := &RecordingSender{}
rs.buffer = make(Stats, 0)
return rs
}
// GetSent returns the stats that have been sent. Locks and copies the current
// state of the sent Stats.
//
// The entire buffer of Stat objects (including Stat.Raw is copied).
func (rs *RecordingSender) GetSent() Stats {
rs.m.Lock()
defer rs.m.Unlock()
results := make(Stats, len(rs.buffer))
for i, e := range rs.buffer {
results[i] = e
results[i].Raw = make([]byte, len(e.Raw))
for j, b := range e.Raw {
results[i].Raw[j] = b
}
}
return results
}
// ClearSent locks the sender and clears any Stats that have been recorded.
func (rs *RecordingSender) ClearSent() {
rs.m.Lock()
defer rs.m.Unlock()
rs.buffer = rs.buffer[:0]
}
// Send parses the provided []byte into stat objects and then appends these to
// the buffer of sent stats. Buffer operations are synchronized so it is safe
// to call this from multiple goroutines (though contenion will impact
// performance so don't use this during a benchmark). Send treats '\n' as a
// delimiter between multiple sats in the same []byte.
//
// Calling after the Sender has been closed will return an error (and not
// mutate the buffer).
func (rs *RecordingSender) Send(data []byte) (int, error) {
sent := ParseStats(data)
rs.m.Lock()
defer rs.m.Unlock()
if rs.closed {
return 0, errors.New("writing to a closed sender")
}
rs.buffer = append(rs.buffer, sent...)
return len(data), nil
}
// Close marks this sender as closed. Subsequent attempts to Send stats will
// result in an error.
func (rs *RecordingSender) Close() error {
rs.m.Lock()
defer rs.m.Unlock()
rs.closed = true
return nil
}

View File

@@ -1,57 +0,0 @@
package statsdtest
import (
"fmt"
"reflect"
"strconv"
"testing"
"time"
"github.com/cactus/go-statsd-client/statsd"
)
func TestRecordingSenderIsSender(t *testing.T) {
// This ensures that if the Sender interface changes in the future we'll get
// compile time failures should the RecordingSender not be updated to meet
// the new definition. This keeps changes from inadvertently breaking tests
// of folks that use go-statsd-client.
var _ statsd.Sender = NewRecordingSender()
}
func TestRecordingSender(t *testing.T) {
start := time.Now()
rs := new(RecordingSender)
statter, err := statsd.NewClientWithSender(rs, "test")
if err != nil {
t.Errorf("failed to construct client")
return
}
statter.Inc("stat", 4444, 1.0)
statter.Dec("stat", 5555, 1.0)
statter.Set("set-stat", "some string", 1.0)
d := time.Since(start)
statter.TimingDuration("timing", d, 1.0)
sent := rs.GetSent()
if len(sent) != 4 {
// just dive out because everything else relies on ordering
t.Fatalf("Did not capture all stats sent; got: %s", sent)
}
ms := float64(d) / float64(time.Millisecond)
// somewhat fragile in that it assums float rendering within client *shrug*
msStr := string(strconv.AppendFloat([]byte(""), ms, 'f', -1, 64))
expected := Stats{
{[]byte("test.stat:4444|c"), "test.stat", "4444", "c", "", true},
{[]byte("test.stat:-5555|c"), "test.stat", "-5555", "c", "", true},
{[]byte("test.set-stat:some string|s"), "test.set-stat", "some string", "s", "", true},
{[]byte(fmt.Sprintf("test.timing:%s|ms", msStr)), "test.timing", msStr, "ms", "", true},
}
if !reflect.DeepEqual(sent, expected) {
t.Errorf("got: %s, want: %s", sent, expected)
}
}

View File

@@ -1,140 +0,0 @@
package statsdtest
import (
"bytes"
"fmt"
"strings"
)
// Stat contains the raw and extracted stat information from a stat that was
// sent by the RecordingSender. Raw will always have the content that was
// consumed for this specific stat and Parsed will be set if no errors were hit
// pulling information out of it.
type Stat struct {
Raw []byte
Stat string
Value string
Tag string
Rate string
Parsed bool
}
// String fulfils the stringer interface
func (s *Stat) String() string {
return fmt.Sprintf("%s %s %s", s.Stat, s.Value, s.Rate)
}
// ParseStats takes a sequence of bytes destined for a Statsd server and parses
// it out into one or more Stat structs. Each struct includes both the raw
// bytes (copied, so the src []byte may be reused if desired) as well as each
// component it was able to parse out. If parsing was incomplete Stat.Parsed
// will be set to false but no error is returned / kept.
func ParseStats(src []byte) Stats {
d := make([]byte, len(src))
for i, b := range src {
d[i] = b
}
// standard protocol indicates one stat per line
entries := bytes.Split(d, []byte{'\n'})
result := make(Stats, len(entries))
for i, e := range entries {
result[i] = Stat{Raw: e}
ss := &result[i]
// : deliniates the stat name from the stat data
marker := bytes.IndexByte(e, ':')
if marker == -1 {
continue
}
ss.Stat = string(e[0:marker])
// stat data folows ':' with the form {value}|{type tag}[|@{sample rate}]
e = e[marker+1:]
marker = bytes.IndexByte(e, '|')
if marker == -1 {
continue
}
ss.Value = string(e[:marker])
e = e[marker+1:]
marker = bytes.IndexByte(e, '|')
if marker == -1 {
// no sample rate
ss.Tag = string(e)
} else {
ss.Tag = string(e[:marker])
e = e[marker+1:]
if len(e) == 0 || e[0] != '@' {
// sample rate should be prefixed with '@'; bail otherwise
continue
}
ss.Rate = string(e[1:])
}
ss.Parsed = true
}
return result
}
// Stats is a slice of Stat
type Stats []Stat
// Unparsed returns any stats that were unable to be completely parsed.
func (s Stats) Unparsed() Stats {
var r Stats
for _, e := range s {
if !e.Parsed {
r = append(r, e)
}
}
return r
}
// CollectNamed returns all data sent for a given stat name.
func (s Stats) CollectNamed(statName string) Stats {
return s.Collect(func(e Stat) bool {
return e.Stat == statName
})
}
// Collect gathers all stats that make some predicate true.
func (s Stats) Collect(pred func(Stat) bool) Stats {
var r Stats
for _, e := range s {
if pred(e) {
r = append(r, e)
}
}
return r
}
// Values returns the values associated with this Stats object.
func (s Stats) Values() []string {
if len(s) == 0 {
return nil
}
r := make([]string, len(s))
for i, e := range s {
r[i] = e.Value
}
return r
}
// String fulfils the stringer interface
func (s Stats) String() string {
if len(s) == 0 {
return ""
}
r := make([]string, len(s))
for i, e := range s {
r[i] = e.String()
}
return strings.Join(r, "\n")
}

View File

@@ -1,154 +0,0 @@
package statsdtest
import (
"bytes"
"reflect"
"testing"
)
type parsingTestCase struct {
name string
sent [][]byte
expected Stats
}
var (
badStatNameOnly = []byte("foo.bar.baz:")
bsnoStat = Stat{
Raw: badStatNameOnly,
Stat: "foo.bar.baz",
Parsed: false,
}
gaugeWithoutRate = []byte("foo.bar.baz:1.000|g")
gworStat = Stat{
Raw: gaugeWithoutRate,
Stat: "foo.bar.baz",
Value: "1.000",
Tag: "g",
Parsed: true,
}
counterWithRate = []byte("foo.bar.baz:1.000|c|@0.75")
cwrStat = Stat{
Raw: counterWithRate,
Stat: "foo.bar.baz",
Value: "1.000",
Tag: "c",
Rate: "0.75",
Parsed: true,
}
stringStat = []byte(":some string value|s")
sStat = Stat{
Raw: stringStat,
Stat: "",
Value: "some string value",
Tag: "s",
Parsed: true,
}
badValue = []byte("asoentuh")
bvStat = Stat{Raw: badValue}
testCases = []parsingTestCase{
{name: "no stat data",
sent: [][]byte{badStatNameOnly},
expected: Stats{bsnoStat}},
{name: "trivial case",
sent: [][]byte{gaugeWithoutRate},
expected: Stats{gworStat}},
{name: "multiple simple",
sent: [][]byte{gaugeWithoutRate, counterWithRate},
expected: Stats{gworStat, cwrStat}},
{name: "mixed good and bad",
sent: [][]byte{badValue, badValue, stringStat, badValue, counterWithRate, badValue},
expected: Stats{bvStat, bvStat, sStat, bvStat, cwrStat, bvStat}},
}
)
func TestParseBytes(t *testing.T) {
for _, tc := range testCases {
got := ParseStats(bytes.Join(tc.sent, []byte("\n")))
want := tc.expected
if !reflect.DeepEqual(got, want) {
t.Errorf("%s: got: %+v, want: %+v", tc.name, got, want)
}
}
}
func TestStatsUnparsed(t *testing.T) {
start := Stats{bsnoStat, gworStat, bsnoStat, bsnoStat, cwrStat}
got := start.Unparsed()
want := Stats{bsnoStat, bsnoStat, bsnoStat}
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %+v, want: %+v", got, want)
}
}
func TestStatsCollectNamed(t *testing.T) {
type test struct {
name string
start Stats
want Stats
matchOn string
}
cases := []test{
{"No matches",
Stats{bsnoStat, cwrStat},
nil,
"foo"},
{"One match",
Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat},
Stats{Stat{Stat: "foo"}},
"foo"},
{"Two matches",
Stats{bsnoStat, Stat{Stat: "foo"}, cwrStat},
Stats{bsnoStat, cwrStat},
"foo.bar.baz"},
}
for _, c := range cases {
got := c.start.CollectNamed(c.matchOn)
if !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want)
}
}
}
func TestStatsCollect(t *testing.T) {
type test struct {
name string
start Stats
want Stats
pred func(Stat) bool
}
cases := []test{
{"Not called",
Stats{},
nil,
func(_ Stat) bool { t.Errorf("should not be called"); return true }},
{"Matches value = 1.000",
Stats{bsnoStat, gworStat, cwrStat, sStat, bsnoStat},
Stats{gworStat, cwrStat},
func(s Stat) bool { return s.Value == "1.000" }},
}
for _, c := range cases {
got := c.start.Collect(c.pred)
if !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: got: %+v, want: %+v", c.name, got, c.want)
}
}
}
func TestStatsValues(t *testing.T) {
start := Stats{bsnoStat, sStat, gworStat}
got := start.Values()
want := []string{bsnoStat.Value, sStat.Value, gworStat.Value}
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %+v, want: %+v", got, want)
}
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import (
"fmt"
"regexp"
)
// The ValidatorFunc type defines a function that can serve
// as a stat name validation function.
type ValidatorFunc func(string) error
var safeName = regexp.MustCompile(`^[a-zA-Z0-9\-_.]+$`)
// CheckName may be used to validate whether a stat name contains invalid
// characters. If invalid characters are found, the function will return an
// error.
func CheckName(stat string) error {
if !safeName.MatchString(stat) {
return fmt.Errorf("invalid stat name: %s", stat)
}
return nil
}

View File

@@ -1,30 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package statsd
import "testing"
var validatorTests = []struct {
Stat string
Valid bool
}{
{"test.one", true},
{"test#two", false},
{"test|three", false},
{"test@four", false},
}
func TestValidator(t *testing.T) {
var err error
for _, tt := range validatorTests {
err = CheckName(tt.Stat)
switch {
case err != nil && tt.Valid:
t.Fatal(err)
case err == nil && !tt.Valid:
t.Fatalf("validation should have failed for %s", tt.Stat)
}
}
}

View File

@@ -1,113 +0,0 @@
// Copyright (c) 2012-2016 Eli Janssen
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/cactus/go-statsd-client/statsd"
flags "github.com/jessevdk/go-flags"
)
func main() {
// command line flags
var opts struct {
HostPort string `long:"host" default:"127.0.0.1:8125" description:"host:port of statsd server"`
Prefix string `long:"prefix" default:"test-client" description:"Statsd prefix"`
StatType string `long:"type" default:"count" description:"stat type to send. Can be one of: timing, count, guage"`
StatValue int64 `long:"value" default:"1" description:"Value to send"`
Name string `short:"n" long:"name" default:"counter" description:"stat name"`
Rate float32 `short:"r" long:"rate" default:"1.0" description:"sample rate"`
Volume int `short:"c" long:"count" default:"1000" description:"Number of stats to send. Volume."`
Nil bool `long:"nil" description:"Use nil client"`
Buffered bool `long:"buffered" description:"Use a buffered client"`
Duration time.Duration `short:"d" long:"duration" default:"10s" description:"How long to spread the volume across. For each second of duration, volume/seconds events will be sent."`
}
// parse said flags
_, err := flags.Parse(&opts)
if err != nil {
if e, ok := err.(*flags.Error); ok {
if e.Type == flags.ErrHelp {
os.Exit(0)
}
}
fmt.Printf("Error: %+v\n", err)
os.Exit(1)
}
if opts.Nil && opts.Buffered {
fmt.Printf("Specifying both nil and buffered together is invalid\n")
os.Exit(1)
}
if opts.Name == "" || statsd.CheckName(opts.Name) != nil {
fmt.Printf("Stat name contains invalid characters\n")
os.Exit(1)
}
if statsd.CheckName(opts.Prefix) != nil {
fmt.Printf("Stat prefix contains invalid characters\n")
os.Exit(1)
}
var client statsd.Statter
if !opts.Nil {
if !opts.Buffered {
client, err = statsd.NewClient(opts.HostPort, opts.Prefix)
} else {
client, err = statsd.NewBufferedClient(opts.HostPort, opts.Prefix, opts.Duration/time.Duration(4), 0)
}
if err != nil {
log.Fatal(err)
}
defer client.Close()
}
var stat func(stat string, value int64, rate float32) error
switch opts.StatType {
case "count":
stat = func(stat string, value int64, rate float32) error {
return client.Inc(stat, value, rate)
}
case "gauge":
stat = func(stat string, value int64, rate float32) error {
return client.Gauge(stat, value, rate)
}
case "timing":
stat = func(stat string, value int64, rate float32) error {
return client.Timing(stat, value, rate)
}
default:
log.Fatal("Unsupported state type")
}
pertick := opts.Volume / int(opts.Duration.Seconds()) / 10
// add some extra time, because the first tick takes a while
ender := time.After(opts.Duration + 100*time.Millisecond)
c := time.Tick(time.Second / 10)
count := 0
for {
select {
case <-c:
for x := 0; x < pertick; x++ {
err := stat(opts.Name, opts.StatValue, opts.Rate)
if err != nil {
log.Printf("Got Error: %+v\n", err)
break
}
count++
}
case <-ender:
log.Printf("%d events called\n", count)
os.Exit(0)
return
}
}
}

View File

@@ -1,2 +0,0 @@
/coverage
/bin

View File

@@ -1,11 +0,0 @@
language: go
go:
- 1.6
- 1.7
- 1.8
- tip
install:
- go get github.com/golang/lint/golint
- go get -v -t ./twitter
script:
- ./test

View File

@@ -1,270 +0,0 @@
# go-twitter [![Build Status](https://travis-ci.org/dghubble/go-twitter.png)](https://travis-ci.org/dghubble/go-twitter) [![GoDoc](https://godoc.org/github.com/dghubble/go-twitter?status.png)](https://godoc.org/github.com/dghubble/go-twitter)
<img align="right" src="https://storage.googleapis.com/dghubble/gopher-on-bird.png">
go-twitter is a Go client library for the [Twitter API](https://dev.twitter.com/rest/public). Check the [usage](#usage) section or try the [examples](/examples) to see how to access the Twitter API.
### Features
* Twitter REST API:
* Accounts
* Direct Messages
* Favorites
* Friends
* Friendships
* Followers
* Search
* Statuses
* Timelines
* Users
* Twitter Streaming API
* Public Streams
* User Streams
* Site Streams
* Firehose Streams
## Install
go get github.com/dghubble/go-twitter/twitter
## Documentation
Read [GoDoc](https://godoc.org/github.com/dghubble/go-twitter/twitter)
## Usage
### REST API
The `twitter` package provides a `Client` for accessing the Twitter API. Here are some example requests.
```go
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
httpClient := config.Client(oauth1.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
// Home Timeline
tweets, resp, err := client.Timelines.HomeTimeline(&twitter.HomeTimelineParams{
Count: 20,
})
// Send a Tweet
tweet, resp, err := client.Statuses.Update("just setting up my twttr", nil)
// Status Show
tweet, resp, err := client.Statuses.Show(585613041028431872, nil)
// Search Tweets
search, resp, err := client.Search.Tweets(&twitter.SearchTweetParams{
Query: "gopher",
})
// User Show
user, resp, err := client.Users.Show(&twitter.UserShowParams{
ScreenName: "dghubble",
})
// Followers
followers, resp, err := client.Followers.List(&twitter.FollowerListParams{})
```
Authentication is handled by the `http.Client` passed to `NewClient` to handle user auth (OAuth1) or application auth (OAuth2). See the [Authentication](#authentication) section.
Required parameters are passed as positional arguments. Optional parameters are passed typed params structs (or nil).
## Streaming API
The Twitter Public, User, Site, and Firehose Streaming APIs can be accessed through the `Client` `StreamService` which provides methods `Filter`, `Sample`, `User`, `Site`, and `Firehose`.
Create a `Client` with an authenticated `http.Client`. All stream endpoints require a user auth context so choose an OAuth1 `http.Client`.
client := twitter.NewClient(httpClient)
Next, request a managed `Stream` be started.
#### Filter
Filter Streams return Tweets that match one or more filtering predicates such as `Track`, `Follow`, and `Locations`.
```go
params := &twitter.StreamFilterParams{
Track: []string{"kitten"},
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Filter(params)
```
#### User
User Streams provide messages specific to the authenticate User and possibly those they follow.
```go
params := &twitter.StreamUserParams{
With: "followings",
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.User(params)
```
*Note* To see Direct Message events, your consumer application must ask Users for read/write/DM access to their account.
#### Sample
Sample Streams return a small sample of public Tweets.
```go
params := &twitter.StreamSampleParams{
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Sample(params)
```
#### Site, Firehose
Site and Firehose Streams require your application to have special permissions, but their API works the same way.
### Receiving Messages
Each `Stream` maintains the connection to the Twitter Streaming API endpoint, receives messages, and sends them on the `Stream.Messages` channel.
Go channels support range iterations which allow you to read the messages which are of type `interface{}`.
```go
for message := range stream.Messages {
fmt.Println(message)
}
```
If you run this in your main goroutine, it will receive messages forever unless the stream stops. To continue execution, receive messages using a separate goroutine.
### Demux
Receiving messages of type `interface{}` isn't very nice, it means you'll have to type switch and probably filter out message types you don't care about.
For this, try a `Demux`, like `SwitchDemux`, which receives messages and type switches them to call functions with typed messages.
For example, say you're only interested in Tweets and Direct Messages.
```go
demux := twitter.NewSwitchDemux()
demux.Tweet = func(tweet *twitter.Tweet) {
fmt.Println(tweet.Text)
}
demux.DM = func(dm *twitter.DirectMessage) {
fmt.Println(dm.SenderID)
}
```
Pass the `Demux` each message or give it the entire `Stream.Message` channel.
```go
for message := range stream.Messages {
demux.Handle(message)
}
// or pass the channel
demux.HandleChan(stream.Messages)
```
### Stopping
The `Stream` will stop itself if the stream disconnects and retrying produces unrecoverable errors. When this occurs, `Stream` will close the `stream.Messages` channel, so execution will break out of any message *for range* loops.
When you are finished receiving from a `Stream`, call `Stop()` which closes the connection, channels, and stops the goroutine **before** returning. This ensures resources are properly cleaned up.
### Pitfalls
**Bad**: In this example, `Stop()` is unlikely to be reached. Control stays in the message loop unless the `Stream` becomes disconnected and cannot retry.
```go
// program does not terminate :(
stream, _ := client.Streams.Sample(params)
for message := range stream.Messages {
demux.Handle(message)
}
stream.Stop()
```
**Bad**: Here, messages are received on a non-main goroutine, but then `Stop()` is called immediately. The `Stream` is stopped and the program exits.
```go
// got no messages :(
stream, _ := client.Streams.Sample(params)
go demux.HandleChan(stream.Messages)
stream.Stop()
```
**Good**: For main package scripts, one option is to receive messages in a goroutine and wait for CTRL-C to be pressed, then explicitly stop the `Stream`.
```go
stream, err := client.Streams.Sample(params)
go demux.HandleChan(stream.Messages)
// Wait for SIGINT and SIGTERM (HIT CTRL-C)
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
stream.Stop()
```
## Authentication
The API client accepts an any `http.Client` capable of making user auth (OAuth1) or application auth (OAuth2) authorized requests. See the [dghubble/oauth1](https://github.com/dghubble/oauth1) and [golang/oauth2](https://github.com/golang/oauth2/) packages which can provide such agnostic clients.
Passing an `http.Client` directly grants you control over the underlying transport, avoids dependencies on particular OAuth1 or OAuth2 packages, and keeps client APIs separate from authentication protocols.
See the [google/go-github](https://github.com/google/go-github) client which takes the same approach.
For example, make requests as a consumer application on behalf of a user who has granted access, with OAuth1.
```go
// OAuth1
import (
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
```
If no user auth context is needed, make requests as your application with application auth.
```go
// OAuth2
import (
"github.com/dghubble/go-twitter/twitter"
"golang.org/x/oauth2"
)
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: accessToken}
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth2.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
```
To implement Login with Twitter for web or mobile, see the gologin [package](https://github.com/dghubble/gologin) and [examples](https://github.com/dghubble/gologin/tree/master/examples/twitter).
## Roadmap
* Support gzipped streams
* Auto-stop streams in the event of long stalls
## Contributing
See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7).
## License
[MIT License](LICENSE)

View File

@@ -1,42 +0,0 @@
# Examples
Get the dependencies and examples
cd examples
go get .
## User Auth (OAuth1)
A user access token (OAuth1) grants a consumer application access to a user's Twitter resources.
Setup an OAuth1 `http.Client` with the consumer key and secret and oauth token and secret.
export TWITTER_CONSUMER_KEY=xxx
export TWITTER_CONSUMER_SECRET=xxx
export TWITTER_ACCESS_TOKEN=xxx
export TWITTER_ACCESS_SECRET=xxx
To make requests as an application, on behalf of a user, create a `twitter` `Client` to get the home timeline, mention timeline, and more (example will **not** post Tweets).
go run user-auth.go
## App Auth (OAuth2)
An application access token (OAuth2) allows an application to make Twitter API requests for public content, with rate limits counting against the app itself. App auth requests can be made to API endpoints which do not require a user context.
Setup an OAuth2 `http.Client` with the Twitter application access token.
export TWITTER_APP_ACCESS_TOKEN=xxx
To make requests as an application, create a `twitter` `Client` and get public Tweets or timelines or other public content.
go run app-auth.go
## Streaming API
A user access token (OAuth1) is required for Streaming API requests. See above.
go run streaming.go
Hit CTRL-C to stop streaming. Uncomment different examples in code to try different streams.

View File

@@ -1,71 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/go-twitter/twitter"
"golang.org/x/oauth2"
)
func main() {
flags := flag.NewFlagSet("app-auth", flag.ExitOnError)
accessToken := flags.String("app-access-token", "", "Twitter Application Access Token")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "TWITTER")
if *accessToken == "" {
log.Fatal("Application Access Token required")
}
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: *accessToken}
// OAuth2 http.Client will automatically authorize Requests
httpClient := config.Client(oauth2.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
// user show
userShowParams := &twitter.UserShowParams{ScreenName: "golang"}
user, _, _ := client.Users.Show(userShowParams)
fmt.Printf("USERS SHOW:\n%+v\n", user)
// users lookup
userLookupParams := &twitter.UserLookupParams{ScreenName: []string{"golang", "gophercon"}}
users, _, _ := client.Users.Lookup(userLookupParams)
fmt.Printf("USERS LOOKUP:\n%+v\n", users)
// status show
statusShowParams := &twitter.StatusShowParams{}
tweet, _, _ := client.Statuses.Show(584077528026849280, statusShowParams)
fmt.Printf("STATUSES SHOW:\n%+v\n", tweet)
// statuses lookup
statusLookupParams := &twitter.StatusLookupParams{ID: []int64{20}, TweetMode: "extended"}
tweets, _, _ := client.Statuses.Lookup([]int64{573893817000140800}, statusLookupParams)
fmt.Printf("STATUSES LOOKUP:\n%+v\n", tweets)
// oEmbed status
statusOembedParams := &twitter.StatusOEmbedParams{ID: 691076766878691329, MaxWidth: 500}
oembed, _, _ := client.Statuses.OEmbed(statusOembedParams)
fmt.Printf("OEMBED TWEET:\n%+v\n", oembed)
// user timeline
userTimelineParams := &twitter.UserTimelineParams{ScreenName: "golang", Count: 2}
tweets, _, _ = client.Timelines.UserTimeline(userTimelineParams)
fmt.Printf("USER TIMELINE:\n%+v\n", tweets)
// search tweets
searchTweetParams := &twitter.SearchTweetParams{
Query: "happy birthday",
TweetMode: "extended",
Count: 3,
}
search, _, _ := client.Search.Tweets(searchTweetParams)
fmt.Printf("SEARCH TWEETS:\n%+v\n", search)
fmt.Printf("SEARCH METADATA:\n%+v\n", search.Metadata)
}

View File

@@ -1,91 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
func main() {
flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
accessToken := flags.String("access-token", "", "Twitter Access Token")
accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "TWITTER")
if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
log.Fatal("Consumer key/secret and Access token/secret required")
}
config := oauth1.NewConfig(*consumerKey, *consumerSecret)
token := oauth1.NewToken(*accessToken, *accessSecret)
// OAuth1 http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter Client
client := twitter.NewClient(httpClient)
// Convenience Demux demultiplexed stream messages
demux := twitter.NewSwitchDemux()
demux.Tweet = func(tweet *twitter.Tweet) {
fmt.Println(tweet.Text)
}
demux.DM = func(dm *twitter.DirectMessage) {
fmt.Println(dm.SenderID)
}
demux.Event = func(event *twitter.Event) {
fmt.Printf("%#v\n", event)
}
fmt.Println("Starting Stream...")
// FILTER
filterParams := &twitter.StreamFilterParams{
Track: []string{"cat"},
StallWarnings: twitter.Bool(true),
}
stream, err := client.Streams.Filter(filterParams)
if err != nil {
log.Fatal(err)
}
// USER (quick test: auth'd user likes a tweet -> event)
// userParams := &twitter.StreamUserParams{
// StallWarnings: twitter.Bool(true),
// With: "followings",
// Language: []string{"en"},
// }
// stream, err := client.Streams.User(userParams)
// if err != nil {
// log.Fatal(err)
// }
// SAMPLE
// sampleParams := &twitter.StreamSampleParams{
// StallWarnings: twitter.Bool(true),
// }
// stream, err := client.Streams.Sample(sampleParams)
// if err != nil {
// log.Fatal(err)
// }
// Receive messages until stopped or stream quits
go demux.HandleChan(stream.Messages)
// Wait for SIGINT and SIGTERM (HIT CTRL-C)
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-ch)
fmt.Println("Stopping Stream...")
stream.Stop()
}

View File

@@ -1,70 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"github.com/coreos/pkg/flagutil"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
func main() {
flags := flag.NewFlagSet("user-auth", flag.ExitOnError)
consumerKey := flags.String("consumer-key", "", "Twitter Consumer Key")
consumerSecret := flags.String("consumer-secret", "", "Twitter Consumer Secret")
accessToken := flags.String("access-token", "", "Twitter Access Token")
accessSecret := flags.String("access-secret", "", "Twitter Access Secret")
flags.Parse(os.Args[1:])
flagutil.SetFlagsFromEnv(flags, "TWITTER")
if *consumerKey == "" || *consumerSecret == "" || *accessToken == "" || *accessSecret == "" {
log.Fatal("Consumer key/secret and Access token/secret required")
}
config := oauth1.NewConfig(*consumerKey, *consumerSecret)
token := oauth1.NewToken(*accessToken, *accessSecret)
// OAuth1 http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// Twitter client
client := twitter.NewClient(httpClient)
// Verify Credentials
verifyParams := &twitter.AccountVerifyParams{
SkipStatus: twitter.Bool(true),
IncludeEmail: twitter.Bool(true),
}
user, _, _ := client.Accounts.VerifyCredentials(verifyParams)
fmt.Printf("User's ACCOUNT:\n%+v\n", user)
// Home Timeline
homeTimelineParams := &twitter.HomeTimelineParams{
Count: 2,
TweetMode: "extended",
}
tweets, _, _ := client.Timelines.HomeTimeline(homeTimelineParams)
fmt.Printf("User's HOME TIMELINE:\n%+v\n", tweets)
// Mention Timeline
mentionTimelineParams := &twitter.MentionTimelineParams{
Count: 2,
TweetMode: "extended",
}
tweets, _, _ = client.Timelines.MentionTimeline(mentionTimelineParams)
fmt.Printf("User's MENTION TIMELINE:\n%+v\n", tweets)
// Retweets of Me Timeline
retweetTimelineParams := &twitter.RetweetsOfMeTimelineParams{
Count: 2,
TweetMode: "extended",
}
tweets, _, _ = client.Timelines.RetweetsOfMeTimeline(retweetTimelineParams)
fmt.Printf("User's 'RETWEETS OF ME' TIMELINE:\n%+v\n", tweets)
// Update (POST!) Tweet (uncomment to run)
// tweet, _, _ := client.Statuses.Update("just setting up my twttr", nil)
// fmt.Printf("Posted Tweet\n%v\n", tweet)
}

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -e
PKGS=$(go list ./... | grep -v /examples)
FORMATTABLE="$(ls -d */)"
LINTABLE=$(go list ./...)
go test $PKGS -cover
go vet $PKGS
echo "Checking gofmt..."
fmtRes=$(gofmt -l $FORMATTABLE)
if [ -n "${fmtRes}" ]; then
echo -e "gofmt checking failed:\n${fmtRes}"
exit 2
fi
echo "Checking golint..."
lintRes=$(echo $LINTABLE | xargs -n 1 golint)
if [ -n "${lintRes}" ]; then
echo -e "golint checking failed:\n${lintRes}"
exit 2
fi

View File

@@ -1,37 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// AccountService provides a method for account credential verification.
type AccountService struct {
sling *sling.Sling
}
// newAccountService returns a new AccountService.
func newAccountService(sling *sling.Sling) *AccountService {
return &AccountService{
sling: sling.Path("account/"),
}
}
// AccountVerifyParams are the params for AccountService.VerifyCredentials.
type AccountVerifyParams struct {
IncludeEntities *bool `url:"include_entities,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
IncludeEmail *bool `url:"include_email,omitempty"`
}
// VerifyCredentials returns the authorized user if credentials are valid and
// returns an error otherwise.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/account/verify_credentials
func (s *AccountService) VerifyCredentials(params *AccountVerifyParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Get("verify_credentials.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}

View File

@@ -1,27 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAccountService_VerifyCredentials(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/account/verify_credentials.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"include_entities": "false", "include_email": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"name": "Dalton Hubble", "id": 623265148}`)
})
client := NewClient(httpClient)
user, _, err := client.Accounts.VerifyCredentials(&AccountVerifyParams{IncludeEntities: Bool(false), IncludeEmail: Bool(true)})
expected := &User{Name: "Dalton Hubble", ID: 623265148}
assert.Nil(t, err)
assert.Equal(t, expected, user)
}

View File

@@ -1,25 +0,0 @@
package twitter
import (
"time"
"github.com/cenkalti/backoff"
)
func newExponentialBackOff() *backoff.ExponentialBackOff {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 5 * time.Second
b.Multiplier = 2.0
b.MaxInterval = 320 * time.Second
b.Reset()
return b
}
func newAggressiveExponentialBackOff() *backoff.ExponentialBackOff {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 1 * time.Minute
b.Multiplier = 2.0
b.MaxInterval = 16 * time.Minute
b.Reset()
return b
}

View File

@@ -1,37 +0,0 @@
package twitter
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewExponentialBackOff(t *testing.T) {
b := newExponentialBackOff()
assert.Equal(t, 5*time.Second, b.InitialInterval)
assert.Equal(t, 2.0, b.Multiplier)
assert.Equal(t, 320*time.Second, b.MaxInterval)
}
func TestNewAggressiveExponentialBackOff(t *testing.T) {
b := newAggressiveExponentialBackOff()
assert.Equal(t, 1*time.Minute, b.InitialInterval)
assert.Equal(t, 2.0, b.Multiplier)
assert.Equal(t, 16*time.Minute, b.MaxInterval)
}
// BackoffRecorder is an implementation of backoff.BackOff that records
// calls to NextBackOff and Reset for later inspection in tests.
type BackOffRecorder struct {
Count int
}
func (b *BackOffRecorder) NextBackOff() time.Duration {
b.Count++
return 1 * time.Nanosecond
}
func (b *BackOffRecorder) Reset() {
b.Count = 0
}

View File

@@ -1,88 +0,0 @@
package twitter
// A Demux receives interface{} messages individually or from a channel and
// sends those messages to one or more outputs determined by the
// implementation.
type Demux interface {
Handle(message interface{})
HandleChan(messages <-chan interface{})
}
// SwitchDemux receives messages and uses a type switch to send each typed
// message to a handler function.
type SwitchDemux struct {
All func(message interface{})
Tweet func(tweet *Tweet)
DM func(dm *DirectMessage)
StatusDeletion func(deletion *StatusDeletion)
LocationDeletion func(LocationDeletion *LocationDeletion)
StreamLimit func(limit *StreamLimit)
StatusWithheld func(statusWithheld *StatusWithheld)
UserWithheld func(userWithheld *UserWithheld)
StreamDisconnect func(disconnect *StreamDisconnect)
Warning func(warning *StallWarning)
FriendsList func(friendsList *FriendsList)
Event func(event *Event)
Other func(message interface{})
}
// NewSwitchDemux returns a new SwitchMux which has NoOp handler functions.
func NewSwitchDemux() SwitchDemux {
return SwitchDemux{
All: func(message interface{}) {},
Tweet: func(tweet *Tweet) {},
DM: func(dm *DirectMessage) {},
StatusDeletion: func(deletion *StatusDeletion) {},
LocationDeletion: func(LocationDeletion *LocationDeletion) {},
StreamLimit: func(limit *StreamLimit) {},
StatusWithheld: func(statusWithheld *StatusWithheld) {},
UserWithheld: func(userWithheld *UserWithheld) {},
StreamDisconnect: func(disconnect *StreamDisconnect) {},
Warning: func(warning *StallWarning) {},
FriendsList: func(friendsList *FriendsList) {},
Event: func(event *Event) {},
Other: func(message interface{}) {},
}
}
// Handle determines the type of a message and calls the corresponding receiver
// function with the typed message. All messages are passed to the All func.
// Messages with unmatched types are passed to the Other func.
func (d SwitchDemux) Handle(message interface{}) {
d.All(message)
switch msg := message.(type) {
case *Tweet:
d.Tweet(msg)
case *DirectMessage:
d.DM(msg)
case *StatusDeletion:
d.StatusDeletion(msg)
case *LocationDeletion:
d.LocationDeletion(msg)
case *StreamLimit:
d.StreamLimit(msg)
case *StatusWithheld:
d.StatusWithheld(msg)
case *UserWithheld:
d.UserWithheld(msg)
case *StreamDisconnect:
d.StreamDisconnect(msg)
case *StallWarning:
d.Warning(msg)
case *FriendsList:
d.FriendsList(msg)
case *Event:
d.Event(msg)
default:
d.Other(msg)
}
}
// HandleChan receives messages and calls the corresponding receiver function
// with the typed message. All messages are passed to the All func. Messages
// with unmatched type are passed to the Other func.
func (d SwitchDemux) HandleChan(messages <-chan interface{}) {
for message := range messages {
d.Handle(message)
}
}

View File

@@ -1,135 +0,0 @@
package twitter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDemux_Handle(t *testing.T) {
messages, expectedCounts := exampleMessages()
counts := &counter{}
demux := newCounterDemux(counts)
for _, message := range messages {
demux.Handle(message)
}
assert.Equal(t, expectedCounts, counts)
}
func TestDemux_HandleChan(t *testing.T) {
messages, expectedCounts := exampleMessages()
counts := &counter{}
demux := newCounterDemux(counts)
ch := make(chan interface{})
// stream messages into channel
go func() {
for _, msg := range messages {
ch <- msg
}
close(ch)
}()
// handle channel messages until exhausted
demux.HandleChan(ch)
assert.Equal(t, expectedCounts, counts)
}
// counter counts stream messages by type for testing.
type counter struct {
all int
tweet int
dm int
statusDeletion int
locationDeletion int
streamLimit int
statusWithheld int
userWithheld int
streamDisconnect int
stallWarning int
friendsList int
event int
other int
}
// newCounterDemux returns a Demux which counts message types.
func newCounterDemux(counter *counter) Demux {
demux := NewSwitchDemux()
demux.All = func(interface{}) {
counter.all++
}
demux.Tweet = func(*Tweet) {
counter.tweet++
}
demux.DM = func(*DirectMessage) {
counter.dm++
}
demux.StatusDeletion = func(*StatusDeletion) {
counter.statusDeletion++
}
demux.LocationDeletion = func(*LocationDeletion) {
counter.locationDeletion++
}
demux.StreamLimit = func(*StreamLimit) {
counter.streamLimit++
}
demux.StatusWithheld = func(*StatusWithheld) {
counter.statusWithheld++
}
demux.UserWithheld = func(*UserWithheld) {
counter.userWithheld++
}
demux.StreamDisconnect = func(*StreamDisconnect) {
counter.streamDisconnect++
}
demux.Warning = func(*StallWarning) {
counter.stallWarning++
}
demux.FriendsList = func(*FriendsList) {
counter.friendsList++
}
demux.Event = func(*Event) {
counter.event++
}
demux.Other = func(interface{}) {
counter.other++
}
return demux
}
// examples messages returns a test stream of messages and the expected
// counts of each message type.
func exampleMessages() (messages []interface{}, expectedCounts *counter) {
var (
tweet = &Tweet{}
dm = &DirectMessage{}
statusDeletion = &StatusDeletion{}
locationDeletion = &LocationDeletion{}
streamLimit = &StreamLimit{}
statusWithheld = &StatusWithheld{}
userWithheld = &UserWithheld{}
streamDisconnect = &StreamDisconnect{}
stallWarning = &StallWarning{}
friendsList = &FriendsList{}
event = &Event{}
otherA = func() {}
otherB = struct{}{}
)
messages = []interface{}{tweet, dm, statusDeletion, locationDeletion,
streamLimit, statusWithheld, userWithheld, streamDisconnect,
stallWarning, friendsList, event, otherA, otherB}
expectedCounts = &counter{
all: len(messages),
tweet: 1,
dm: 1,
statusDeletion: 1,
locationDeletion: 1,
streamLimit: 1,
statusWithheld: 1,
userWithheld: 1,
streamDisconnect: 1,
stallWarning: 1,
friendsList: 1,
event: 1,
other: 2,
}
return messages, expectedCounts
}

View File

@@ -1,130 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// DirectMessage is a direct message to a single recipient.
type DirectMessage struct {
CreatedAt string `json:"created_at"`
Entities *Entities `json:"entities"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
Recipient *User `json:"recipient"`
RecipientID int64 `json:"recipient_id"`
RecipientScreenName string `json:"recipient_screen_name"`
Sender *User `json:"sender"`
SenderID int64 `json:"sender_id"`
SenderScreenName string `json:"sender_screen_name"`
Text string `json:"text"`
}
// DirectMessageService provides methods for accessing Twitter direct message
// API endpoints.
type DirectMessageService struct {
baseSling *sling.Sling
sling *sling.Sling
}
// newDirectMessageService returns a new DirectMessageService.
func newDirectMessageService(sling *sling.Sling) *DirectMessageService {
return &DirectMessageService{
baseSling: sling.New(),
sling: sling.Path("direct_messages/"),
}
}
// directMessageShowParams are the parameters for DirectMessageService.Show
type directMessageShowParams struct {
ID int64 `url:"id,omitempty"`
}
// Show returns the requested Direct Message.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/get/direct_messages/show
func (s *DirectMessageService) Show(id int64) (*DirectMessage, *http.Response, error) {
params := &directMessageShowParams{ID: id}
dm := new(DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(dm, apiError)
return dm, resp, relevantError(err, *apiError)
}
// DirectMessageGetParams are the parameters for DirectMessageService.Get
type DirectMessageGetParams struct {
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
Count int `url:"count,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
}
// Get returns recent Direct Messages received by the authenticated user.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/get/direct_messages
func (s *DirectMessageService) Get(params *DirectMessageGetParams) ([]DirectMessage, *http.Response, error) {
dms := new([]DirectMessage)
apiError := new(APIError)
resp, err := s.baseSling.New().Get("direct_messages.json").QueryStruct(params).Receive(dms, apiError)
return *dms, resp, relevantError(err, *apiError)
}
// DirectMessageSentParams are the parameters for DirectMessageService.Sent
type DirectMessageSentParams struct {
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
Count int `url:"count,omitempty"`
Page int `url:"page,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
}
// Sent returns recent Direct Messages sent by the authenticated user.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/get/direct_messages/sent
func (s *DirectMessageService) Sent(params *DirectMessageSentParams) ([]DirectMessage, *http.Response, error) {
dms := new([]DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Get("sent.json").QueryStruct(params).Receive(dms, apiError)
return *dms, resp, relevantError(err, *apiError)
}
// DirectMessageNewParams are the parameters for DirectMessageService.New
type DirectMessageNewParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Text string `url:"text"`
}
// New sends a new Direct Message to a specified user as the authenticated
// user.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/post/direct_messages/new
func (s *DirectMessageService) New(params *DirectMessageNewParams) (*DirectMessage, *http.Response, error) {
dm := new(DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Post("new.json").BodyForm(params).Receive(dm, apiError)
return dm, resp, relevantError(err, *apiError)
}
// DirectMessageDestroyParams are the parameters for DirectMessageService.Destroy
type DirectMessageDestroyParams struct {
ID int64 `url:"id,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
}
// Destroy deletes the Direct Message with the given id and returns it if
// successful.
// Requires a user auth context with DM scope.
// https://dev.twitter.com/rest/reference/post/direct_messages/destroy
func (s *DirectMessageService) Destroy(id int64, params *DirectMessageDestroyParams) (*DirectMessage, *http.Response, error) {
if params == nil {
params = &DirectMessageDestroyParams{}
}
params.ID = id
dm := new(DirectMessage)
apiError := new(APIError)
resp, err := s.sling.New().Post("destroy.json").BodyForm(params).Receive(dm, apiError)
return dm, resp, relevantError(err, *apiError)
}

View File

@@ -1,110 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
var (
testDM = DirectMessage{
ID: 240136858829479936,
Recipient: &User{ScreenName: "theSeanCook"},
Sender: &User{ScreenName: "s0c1alm3dia"},
Text: "hello world",
}
testDMIDStr = "240136858829479936"
testDMJSON = `{"id": 240136858829479936,"recipient": {"screen_name": "theSeanCook"},"sender": {"screen_name": "s0c1alm3dia"},"text": "hello world"}`
)
func TestDirectMessageService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": testDMIDStr}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, testDMJSON)
})
client := NewClient(httpClient)
dms, _, err := client.DirectMessages.Show(testDM.ID)
assert.Nil(t, err)
assert.Equal(t, &testDM, dms)
}
func TestDirectMessageService_Get(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"since_id": "589147592367431680", "count": "1"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[`+testDMJSON+`]`)
})
client := NewClient(httpClient)
params := &DirectMessageGetParams{SinceID: 589147592367431680, Count: 1}
dms, _, err := client.DirectMessages.Get(params)
expected := []DirectMessage{testDM}
assert.Nil(t, err)
assert.Equal(t, expected, dms)
}
func TestDirectMessageService_Sent(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/sent.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"since_id": "589147592367431680", "count": "1"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[`+testDMJSON+`]`)
})
client := NewClient(httpClient)
params := &DirectMessageSentParams{SinceID: 589147592367431680, Count: 1}
dms, _, err := client.DirectMessages.Sent(params)
expected := []DirectMessage{testDM}
assert.Nil(t, err)
assert.Equal(t, expected, dms)
}
func TestDirectMessageService_New(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/new.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"screen_name": "theseancook", "text": "hello world"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, testDMJSON)
})
client := NewClient(httpClient)
params := &DirectMessageNewParams{ScreenName: "theseancook", Text: "hello world"}
dm, _, err := client.DirectMessages.New(params)
assert.Nil(t, err)
assert.Equal(t, &testDM, dm)
}
func TestDirectMessageService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/direct_messages/destroy.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"id": testDMIDStr}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, testDMJSON)
})
client := NewClient(httpClient)
dm, _, err := client.DirectMessages.Destroy(testDM.ID, nil)
assert.Nil(t, err)
assert.Equal(t, &testDM, dm)
}

View File

@@ -1,70 +0,0 @@
/*
Package twitter provides a Client for the Twitter API.
The twitter package provides a Client for accessing the Twitter API. Here are
some example requests.
// Twitter client
client := twitter.NewClient(httpClient)
// Home Timeline
tweets, resp, err := client.Timelines.HomeTimeline(&HomeTimelineParams{})
// Send a Tweet
tweet, resp, err := client.Statuses.Update("just setting up my twttr", nil)
// Status Show
tweet, resp, err := client.Statuses.Show(585613041028431872, nil)
// User Show
params := &twitter.UserShowParams{ScreenName: "dghubble"}
user, resp, err := client.Users.Show(params)
// Followers
followers, resp, err := client.Followers.List(&FollowerListParams{})
Required parameters are passed as positional arguments. Optional parameters
are passed in a typed params struct (or pass nil).
Authentication
By design, the Twitter Client accepts any http.Client so user auth (OAuth1) or
application auth (OAuth2) requests can be made by using the appropriate
authenticated client. Use the https://github.com/dghubble/oauth1 and
https://github.com/golang/oauth2 packages to obtain an http.Client which
transparently authorizes requests.
For example, make requests as a consumer application on behalf of a user who
has granted access, with OAuth1.
// OAuth1
import (
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
)
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth1.NoContext, token)
// twitter client
client := twitter.NewClient(httpClient)
If no user auth context is needed, make requests as your application with
application auth.
// OAuth2
import (
"github.com/dghubble/go-twitter/twitter"
"golang.org/x/oauth2"
)
config := &oauth2.Config{}
token := &oauth2.Token{AccessToken: accessToken}
// http.Client will automatically authorize Requests
httpClient := config.Client(oauth2.NoContext, token)
// twitter client
client := twitter.NewClient(httpClient)
To implement Login with Twitter, see https://github.com/dghubble/gologin.
*/
package twitter

View File

@@ -1,104 +0,0 @@
package twitter
// Entities represent metadata and context info parsed from Twitter components.
// https://dev.twitter.com/overview/api/entities
// TODO: symbols
type Entities struct {
Hashtags []HashtagEntity `json:"hashtags"`
Media []MediaEntity `json:"media"`
Urls []URLEntity `json:"urls"`
UserMentions []MentionEntity `json:"user_mentions"`
}
// HashtagEntity represents a hashtag which has been parsed from text.
type HashtagEntity struct {
Indices Indices `json:"indices"`
Text string `json:"text"`
}
// URLEntity represents a URL which has been parsed from text.
type URLEntity struct {
Indices Indices `json:"indices"`
DisplayURL string `json:"display_url"`
ExpandedURL string `json:"expanded_url"`
URL string `json:"url"`
}
// MediaEntity represents media elements associated with a Tweet.
type MediaEntity struct {
URLEntity
ID int64 `json:"id"`
IDStr string `json:"id_str"`
MediaURL string `json:"media_url"`
MediaURLHttps string `json:"media_url_https"`
SourceStatusID int64 `json:"source_status_id"`
SourceStatusIDStr string `json:"source_status_id_str"`
Type string `json:"type"`
Sizes MediaSizes `json:"sizes"`
VideoInfo VideoInfo `json:"video_info"`
}
// MentionEntity represents Twitter user mentions parsed from text.
type MentionEntity struct {
Indices Indices `json:"indices"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
Name string `json:"name"`
ScreenName string `json:"screen_name"`
}
// UserEntities contain Entities parsed from User url and description fields.
// https://dev.twitter.com/overview/api/entities-in-twitter-objects#users
type UserEntities struct {
URL Entities `json:"url"`
Description Entities `json:"description"`
}
// ExtendedEntity contains media information.
// https://dev.twitter.com/overview/api/entities-in-twitter-objects#extended_entities
type ExtendedEntity struct {
Media []MediaEntity `json:"media"`
}
// Indices represent the start and end offsets within text.
type Indices [2]int
// Start returns the index at which an entity starts, inclusive.
func (i Indices) Start() int {
return i[0]
}
// End returns the index at which an entity ends, exclusive.
func (i Indices) End() int {
return i[1]
}
// MediaSizes contain the different size media that are available.
// https://dev.twitter.com/overview/api/entities#obj-sizes
type MediaSizes struct {
Thumb MediaSize `json:"thumb"`
Large MediaSize `json:"large"`
Medium MediaSize `json:"medium"`
Small MediaSize `json:"small"`
}
// MediaSize describes the height, width, and resizing method used.
type MediaSize struct {
Width int `json:"w"`
Height int `json:"h"`
Resize string `json:"resize"`
}
// VideoInfo is available on video media objects.
type VideoInfo struct {
AspectRatio [2]int `json:"aspect_ratio"`
DurationMillis int `json:"duration_millis"`
Variants []VideoVariant `json:"variants"`
}
// VideoVariant describes one of the available video formats.
type VideoVariant struct {
ContentType string `json:"content_type"`
Bitrate int `json:"bitrate"`
URL string `json:"url"`
}

View File

@@ -1,22 +0,0 @@
package twitter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIndices(t *testing.T) {
cases := []struct {
pair Indices
expectedStart int
expectedEnd int
}{
{Indices{}, 0, 0},
{Indices{25, 47}, 25, 47},
}
for _, c := range cases {
assert.Equal(t, c.expectedStart, c.pair.Start())
assert.Equal(t, c.expectedEnd, c.pair.End())
}
}

View File

@@ -1,47 +0,0 @@
package twitter
import (
"fmt"
)
// APIError represents a Twitter API Error response
// https://dev.twitter.com/overview/api/response-codes
type APIError struct {
Errors []ErrorDetail `json:"errors"`
}
// ErrorDetail represents an individual item in an APIError.
type ErrorDetail struct {
Message string `json:"message"`
Code int `json:"code"`
}
func (e APIError) Error() string {
if len(e.Errors) > 0 {
err := e.Errors[0]
return fmt.Sprintf("twitter: %d %v", err.Code, err.Message)
}
return ""
}
// Empty returns true if empty. Otherwise, at least 1 error message/code is
// present and false is returned.
func (e APIError) Empty() bool {
if len(e.Errors) == 0 {
return true
}
return false
}
// relevantError returns any non-nil http-related error (creating the request,
// getting the response, decoding) if any. If the decoded apiError is non-zero
// the apiError is returned. Otherwise, no errors occurred, returns nil.
func relevantError(httpError error, apiError APIError) error {
if httpError != nil {
return httpError
}
if apiError.Empty() {
return nil
}
return apiError
}

View File

@@ -1,48 +0,0 @@
package twitter
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
var errAPI = APIError{
Errors: []ErrorDetail{
ErrorDetail{Message: "Status is a duplicate", Code: 187},
},
}
var errHTTP = fmt.Errorf("unknown host")
func TestAPIError_Error(t *testing.T) {
err := APIError{}
if assert.Error(t, err) {
assert.Equal(t, "", err.Error())
}
if assert.Error(t, errAPI) {
assert.Equal(t, "twitter: 187 Status is a duplicate", errAPI.Error())
}
}
func TestAPIError_Empty(t *testing.T) {
err := APIError{}
assert.True(t, err.Empty())
assert.False(t, errAPI.Empty())
}
func TestRelevantError(t *testing.T) {
cases := []struct {
httpError error
apiError APIError
expected error
}{
{nil, APIError{}, nil},
{nil, errAPI, errAPI},
{errHTTP, APIError{}, errHTTP},
{errHTTP, errAPI, errHTTP},
}
for _, c := range cases {
err := relevantError(c.httpError, c.apiError)
assert.Equal(t, c.expected, err)
}
}

View File

@@ -1,72 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FavoriteService provides methods for accessing Twitter favorite API endpoints.
//
// Note: the like action was known as favorite before November 3, 2015; the
// historical naming remains in API methods and object properties.
type FavoriteService struct {
sling *sling.Sling
}
// newFavoriteService returns a new FavoriteService.
func newFavoriteService(sling *sling.Sling) *FavoriteService {
return &FavoriteService{
sling: sling.Path("favorites/"),
}
}
// FavoriteListParams are the parameters for FavoriteService.List.
type FavoriteListParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// List returns liked Tweets from the specified user.
// https://dev.twitter.com/rest/reference/get/favorites/list
func (s *FavoriteService) List(params *FavoriteListParams) ([]Tweet, *http.Response, error) {
favorites := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(favorites, apiError)
return *favorites, resp, relevantError(err, *apiError)
}
// FavoriteCreateParams are the parameters for FavoriteService.Create.
type FavoriteCreateParams struct {
ID int64 `url:"id,omitempty"`
}
// Create favorites the specified tweet.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/favorites/create
func (s *FavoriteService) Create(params *FavoriteCreateParams) (*Tweet, *http.Response, error) {
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// FavoriteDestroyParams are the parameters for FavoriteService.Destroy.
type FavoriteDestroyParams struct {
ID int64 `url:"id,omitempty"`
}
// Destroy un-favorites the specified tweet.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/favorites/destroy
func (s *FavoriteService) Destroy(params *FavoriteDestroyParams) (*Tweet, *http.Response, error) {
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}

View File

@@ -1,65 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFavoriteService_List(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/favorites/list.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "113419064", "since_id": "101492475", "include_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "Gophercon talks!"}, {"text": "Why gophers are so adorable"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Favorites.List(&FavoriteListParams{UserID: 113419064, SinceID: 101492475, IncludeEntities: Bool(false)})
expected := []Tweet{Tweet{Text: "Gophercon talks!"}, Tweet{Text: "Why gophers are so adorable"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestFavoriteService_Create(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/favorites/create.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very informative tweet"}`)
})
client := NewClient(httpClient)
params := &FavoriteCreateParams{ID: 12345}
tweet, _, err := client.Favorites.Create(params)
assert.Nil(t, err)
expected := &Tweet{ID: 581980947630845953, Text: "very informative tweet"}
assert.Equal(t, expected, tweet)
}
func TestFavoriteService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/favorites/destroy.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very unhappy tweet"}`)
})
client := NewClient(httpClient)
params := &FavoriteDestroyParams{ID: 12345}
tweet, _, err := client.Favorites.Destroy(params)
assert.Nil(t, err)
expected := &Tweet{ID: 581980947630845953, Text: "very unhappy tweet"}
assert.Equal(t, expected, tweet)
}

View File

@@ -1,73 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FollowerIDs is a cursored collection of follower ids.
type FollowerIDs struct {
IDs []int64 `json:"ids"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// Followers is a cursored collection of followers.
type Followers struct {
Users []User `json:"users"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// FollowerService provides methods for accessing Twitter followers endpoints.
type FollowerService struct {
sling *sling.Sling
}
// newFollowerService returns a new FollowerService.
func newFollowerService(sling *sling.Sling) *FollowerService {
return &FollowerService{
sling: sling.Path("followers/"),
}
}
// FollowerIDParams are the parameters for FollowerService.Ids
type FollowerIDParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
}
// IDs returns a cursored collection of user ids following the specified user.
// https://dev.twitter.com/rest/reference/get/followers/ids
func (s *FollowerService) IDs(params *FollowerIDParams) (*FollowerIDs, *http.Response, error) {
ids := new(FollowerIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("ids.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}
// FollowerListParams are the parameters for FollowerService.List
type FollowerListParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
IncludeUserEntities *bool `url:"include_user_entities,omitempty"`
}
// List returns a cursored collection of Users following the specified user.
// https://dev.twitter.com/rest/reference/get/followers/list
func (s *FollowerService) List(params *FollowerListParams) (*Followers, *http.Response, error) {
followers := new(Followers)
apiError := new(APIError)
resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(followers, apiError)
return followers, resp, relevantError(err, *apiError)
}

View File

@@ -1,69 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFollowerService_Ids(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/followers/ids.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "623265148", "count": "5", "cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FollowerIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FollowerIDParams{
UserID: 623265148,
Count: 5,
Cursor: 1516933260114270762,
}
followerIDs, _, err := client.Followers.IDs(params)
assert.Nil(t, err)
assert.Equal(t, expected, followerIDs)
}
func TestFollowerService_List(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/followers/list.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "dghubble", "count": "5", "cursor": "1516933260114270762", "skip_status": "true", "include_user_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"users": [{"id": 123}], "next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &Followers{
Users: []User{User{ID: 123}},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FollowerListParams{
ScreenName: "dghubble",
Count: 5,
Cursor: 1516933260114270762,
SkipStatus: Bool(true),
IncludeUserEntities: Bool(false),
}
followers, _, err := client.Followers.List(params)
assert.Nil(t, err)
assert.Equal(t, expected, followers)
}

View File

@@ -1,73 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FriendIDs is a cursored collection of friend ids.
type FriendIDs struct {
IDs []int64 `json:"ids"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// Friends is a cursored collection of friends.
type Friends struct {
Users []User `json:"users"`
NextCursor int64 `json:"next_cursor"`
NextCursorStr string `json:"next_cursor_str"`
PreviousCursor int64 `json:"previous_cursor"`
PreviousCursorStr string `json:"previous_cursor_str"`
}
// FriendService provides methods for accessing Twitter friends endpoints.
type FriendService struct {
sling *sling.Sling
}
// newFriendService returns a new FriendService.
func newFriendService(sling *sling.Sling) *FriendService {
return &FriendService{
sling: sling.Path("friends/"),
}
}
// FriendIDParams are the parameters for FriendService.Ids
type FriendIDParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
}
// IDs returns a cursored collection of user ids that the specified user is following.
// https://dev.twitter.com/rest/reference/get/friends/ids
func (s *FriendService) IDs(params *FriendIDParams) (*FriendIDs, *http.Response, error) {
ids := new(FriendIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("ids.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}
// FriendListParams are the parameters for FriendService.List
type FriendListParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Cursor int64 `url:"cursor,omitempty"`
Count int `url:"count,omitempty"`
SkipStatus *bool `url:"skip_status,omitempty"`
IncludeUserEntities *bool `url:"include_user_entities,omitempty"`
}
// List returns a cursored collection of Users that the specified user is following.
// https://dev.twitter.com/rest/reference/get/friends/list
func (s *FriendService) List(params *FriendListParams) (*Friends, *http.Response, error) {
friends := new(Friends)
apiError := new(APIError)
resp, err := s.sling.New().Get("list.json").QueryStruct(params).Receive(friends, apiError)
return friends, resp, relevantError(err, *apiError)
}

View File

@@ -1,69 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFriendService_Ids(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friends/ids.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "623265148", "count": "5", "cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FriendIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendIDParams{
UserID: 623265148,
Count: 5,
Cursor: 1516933260114270762,
}
friendIDs, _, err := client.Friends.IDs(params)
assert.Nil(t, err)
assert.Equal(t, expected, friendIDs)
}
func TestFriendService_List(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friends/list.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "dghubble", "count": "5", "cursor": "1516933260114270762", "skip_status": "true", "include_user_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"users": [{"id": 123}], "next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &Friends{
Users: []User{User{ID: 123}},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendListParams{
ScreenName: "dghubble",
Count: 5,
Cursor: 1516933260114270762,
SkipStatus: Bool(true),
IncludeUserEntities: Bool(false),
}
friends, _, err := client.Friends.List(params)
assert.Nil(t, err)
assert.Equal(t, expected, friends)
}

View File

@@ -1,134 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// FriendshipService provides methods for accessing Twitter friendship API
// endpoints.
type FriendshipService struct {
sling *sling.Sling
}
// newFriendshipService returns a new FriendshipService.
func newFriendshipService(sling *sling.Sling) *FriendshipService {
return &FriendshipService{
sling: sling.Path("friendships/"),
}
}
// FriendshipCreateParams are parameters for FriendshipService.Create
type FriendshipCreateParams struct {
ScreenName string `url:"screen_name,omitempty"`
UserID int64 `url:"user_id,omitempty"`
Follow *bool `url:"follow,omitempty"`
}
// Create creates a friendship to (i.e. follows) the specified user and
// returns the followed user.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/friendships/create
func (s *FriendshipService) Create(params *FriendshipCreateParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Post("create.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}
// FriendshipShowParams are paramenters for FriendshipService.Show
type FriendshipShowParams struct {
SourceID int64 `url:"source_id,omitempty"`
SourceScreenName string `url:"source_screen_name,omitempty"`
TargetID int64 `url:"target_id,omitempty"`
TargetScreenName string `url:"target_screen_name,omitempty"`
}
// Show returns the relationship between two arbitrary users.
// Requires a user auth or an app context.
// https://dev.twitter.com/rest/reference/get/friendships/show
func (s *FriendshipService) Show(params *FriendshipShowParams) (*Relationship, *http.Response, error) {
response := new(RelationshipResponse)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(response, apiError)
return response.Relationship, resp, relevantError(err, *apiError)
}
// RelationshipResponse contains a relationship.
type RelationshipResponse struct {
Relationship *Relationship `json:"relationship"`
}
// Relationship represents the relation between a source user and target user.
type Relationship struct {
Source RelationshipSource `json:"source"`
Target RelationshipTarget `json:"target"`
}
// RelationshipSource represents the source user.
type RelationshipSource struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
ScreenName string `json:"screen_name"`
Following bool `json:"following"`
FollowedBy bool `json:"followed_by"`
CanDM bool `json:"can_dm"`
Blocking bool `json:"blocking"`
Muting bool `json:"muting"`
AllReplies bool `json:"all_replies"`
WantRetweets bool `json:"want_retweets"`
MarkedSpam bool `json:"marked_spam"`
NotificationsEnabled bool `json:"notifications_enabled"`
}
// RelationshipTarget represents the target user.
type RelationshipTarget struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
ScreenName string `json:"screen_name"`
Following bool `json:"following"`
FollowedBy bool `json:"followed_by"`
}
// FriendshipDestroyParams are paramenters for FriendshipService.Destroy
type FriendshipDestroyParams struct {
ScreenName string `url:"screen_name,omitempty"`
UserID int64 `url:"user_id,omitempty"`
}
// Destroy destroys a friendship to (i.e. unfollows) the specified user and
// returns the unfollowed user.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/friendships/destroy
func (s *FriendshipService) Destroy(params *FriendshipDestroyParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Post("destroy.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}
// FriendshipPendingParams are paramenters for FriendshipService.Outgoing
type FriendshipPendingParams struct {
Cursor int64 `url:"cursor,omitempty"`
}
// Outgoing returns a collection of numeric IDs for every protected user for whom the authenticating
// user has a pending follow request.
// https://dev.twitter.com/rest/reference/get/friendships/outgoing
func (s *FriendshipService) Outgoing(params *FriendshipPendingParams) (*FriendIDs, *http.Response, error) {
ids := new(FriendIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("outgoing.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}
// Incoming returns a collection of numeric IDs for every user who has a pending request to
// follow the authenticating user.
// https://dev.twitter.com/rest/reference/get/friendships/incoming
func (s *FriendshipService) Incoming(params *FriendshipPendingParams) (*FriendIDs, *http.Response, error) {
ids := new(FriendIDs)
apiError := new(APIError)
resp, err := s.sling.New().Get("incoming.json").QueryStruct(params).Receive(ids, apiError)
return ids, resp, relevantError(err, *apiError)
}

View File

@@ -1,123 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFriendshipService_Create(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/create.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"user_id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 12345, "name": "Doug Williams"}`)
})
client := NewClient(httpClient)
params := &FriendshipCreateParams{UserID: 12345}
user, _, err := client.Friendships.Create(params)
assert.Nil(t, err)
expected := &User{ID: 12345, Name: "Doug Williams"}
assert.Equal(t, expected, user)
}
func TestFriendshipService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"source_screen_name": "foo", "target_screen_name": "bar"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{ "relationship": { "source": { "can_dm": false, "muting": true, "id_str": "8649302", "id": 8649302, "screen_name": "foo"}, "target": { "id_str": "12148", "id": 12148, "screen_name": "bar", "following": true, "followed_by": false } } }`)
})
client := NewClient(httpClient)
params := &FriendshipShowParams{SourceScreenName: "foo", TargetScreenName: "bar"}
relationship, _, err := client.Friendships.Show(params)
assert.Nil(t, err)
expected := &Relationship{
Source: RelationshipSource{ID: 8649302, ScreenName: "foo", IDStr: "8649302", CanDM: false, Muting: true, WantRetweets: false},
Target: RelationshipTarget{ID: 12148, ScreenName: "bar", IDStr: "12148", Following: true, FollowedBy: false},
}
assert.Equal(t, expected, relationship)
}
func TestFriendshipService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/destroy.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertPostForm(t, map[string]string{"user_id": "12345"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 12345, "name": "Doug Williams"}`)
})
client := NewClient(httpClient)
params := &FriendshipDestroyParams{UserID: 12345}
user, _, err := client.Friendships.Destroy(params)
assert.Nil(t, err)
expected := &User{ID: 12345, Name: "Doug Williams"}
assert.Equal(t, expected, user)
}
func TestFriendshipService_Outgoing(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/outgoing.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FriendIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendshipPendingParams{
Cursor: 1516933260114270762,
}
friendIDs, _, err := client.Friendships.Outgoing(params)
assert.Nil(t, err)
assert.Equal(t, expected, friendIDs)
}
func TestFriendshipService_Incoming(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/friendships/incoming.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"cursor": "1516933260114270762"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ids":[178082406,3318241001,1318020818,191714329,376703838],"next_cursor":1516837838944119498,"next_cursor_str":"1516837838944119498","previous_cursor":-1516924983503961435,"previous_cursor_str":"-1516924983503961435"}`)
})
expected := &FriendIDs{
IDs: []int64{178082406, 3318241001, 1318020818, 191714329, 376703838},
NextCursor: 1516837838944119498,
NextCursorStr: "1516837838944119498",
PreviousCursor: -1516924983503961435,
PreviousCursorStr: "-1516924983503961435",
}
client := NewClient(httpClient)
params := &FriendshipPendingParams{
Cursor: 1516933260114270762,
}
friendIDs, _, err := client.Friendships.Incoming(params)
assert.Nil(t, err)
assert.Equal(t, expected, friendIDs)
}

View File

@@ -1,62 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// Search represents the result of a Tweet search.
type Search struct {
Statuses []Tweet `json:"statuses"`
Metadata *SearchMetadata `json:"search_metadata"`
}
// SearchMetadata describes a Search result.
type SearchMetadata struct {
Count int `json:"count"`
SinceID int64 `json:"since_id"`
SinceIDStr string `json:"since_id_str"`
MaxID int64 `json:"max_id"`
MaxIDStr string `json:"max_id_str"`
RefreshURL string `json:"refresh_url"`
NextResults string `json:"next_results"`
CompletedIn float64 `json:"completed_in"`
Query string `json:"query"`
}
// SearchService provides methods for accessing Twitter search API endpoints.
type SearchService struct {
sling *sling.Sling
}
// newSearchService returns a new SearchService.
func newSearchService(sling *sling.Sling) *SearchService {
return &SearchService{
sling: sling.Path("search/"),
}
}
// SearchTweetParams are the parameters for SearchService.Tweets
type SearchTweetParams struct {
Query string `url:"q,omitempty"`
Geocode string `url:"geocode,omitempty"`
Lang string `url:"lang,omitempty"`
Locale string `url:"locale,omitempty"`
ResultType string `url:"result_type,omitempty"`
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
Until string `url:"until,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Tweets returns a collection of Tweets matching a search query.
// https://dev.twitter.com/rest/reference/get/search/tweets
func (s *SearchService) Tweets(params *SearchTweetParams) (*Search, *http.Response, error) {
search := new(Search)
apiError := new(APIError)
resp, err := s.sling.New().Get("tweets.json").QueryStruct(params).Receive(search, apiError)
return search, resp, relevantError(err, *apiError)
}

View File

@@ -1,46 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSearchService_Tweets(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/search/tweets.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"q": "happy birthday", "result_type": "popular", "count": "1"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"statuses":[{"id":781760642139250689}],"search_metadata":{"completed_in":0.043,"max_id":781760642139250689,"max_id_str":"781760642139250689","next_results":"?max_id=781760640104828927&q=happy+birthday&count=1&include_entities=1","query":"happy birthday","refresh_url":"?since_id=781760642139250689&q=happy+birthday&include_entities=1","count":1,"since_id":0,"since_id_str":"0"}}`)
})
client := NewClient(httpClient)
search, _, err := client.Search.Tweets(&SearchTweetParams{
Query: "happy birthday",
Count: 1,
ResultType: "popular",
})
expected := &Search{
Statuses: []Tweet{
Tweet{ID: 781760642139250689},
},
Metadata: &SearchMetadata{
Count: 1,
SinceID: 0,
SinceIDStr: "0",
MaxID: 781760642139250689,
MaxIDStr: "781760642139250689",
RefreshURL: "?since_id=781760642139250689&q=happy+birthday&include_entities=1",
NextResults: "?max_id=781760640104828927&q=happy+birthday&count=1&include_entities=1",
CompletedIn: 0.043,
Query: "happy birthday",
},
}
assert.Nil(t, err)
assert.Equal(t, expected, search)
}

View File

@@ -1,310 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"time"
"github.com/dghubble/sling"
)
// Tweet represents a Twitter Tweet, previously called a status.
// https://dev.twitter.com/overview/api/tweets
type Tweet struct {
Coordinates *Coordinates `json:"coordinates"`
CreatedAt string `json:"created_at"`
CurrentUserRetweet *TweetIdentifier `json:"current_user_retweet"`
Entities *Entities `json:"entities"`
FavoriteCount int `json:"favorite_count"`
Favorited bool `json:"favorited"`
FilterLevel string `json:"filter_level"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
InReplyToScreenName string `json:"in_reply_to_screen_name"`
InReplyToStatusID int64 `json:"in_reply_to_status_id"`
InReplyToStatusIDStr string `json:"in_reply_to_status_id_str"`
InReplyToUserID int64 `json:"in_reply_to_user_id"`
InReplyToUserIDStr string `json:"in_reply_to_user_id_str"`
Lang string `json:"lang"`
PossiblySensitive bool `json:"possibly_sensitive"`
RetweetCount int `json:"retweet_count"`
Retweeted bool `json:"retweeted"`
RetweetedStatus *Tweet `json:"retweeted_status"`
Source string `json:"source"`
Scopes map[string]interface{} `json:"scopes"`
Text string `json:"text"`
FullText string `json:"full_text"`
DisplayTextRange Indices `json:"display_text_range"`
Place *Place `json:"place"`
Truncated bool `json:"truncated"`
User *User `json:"user"`
WithheldCopyright bool `json:"withheld_copyright"`
WithheldInCountries []string `json:"withheld_in_countries"`
WithheldScope string `json:"withheld_scope"`
ExtendedEntities *ExtendedEntity `json:"extended_entities"`
ExtendedTweet *ExtendedTweet `json:"extended_tweet"`
QuotedStatusID int64 `json:"quoted_status_id"`
QuotedStatusIDStr string `json:"quoted_status_id_str"`
QuotedStatus *Tweet `json:"quoted_status"`
}
// CreatedAtTime is a convenience wrapper that returns the Created_at time, parsed as a time.Time struct
func (t Tweet) CreatedAtTime() (time.Time, error) {
return time.Parse(time.RubyDate, t.CreatedAt)
}
// ExtendedTweet represents fields embedded in extended Tweets when served in
// compatibility mode (default).
// https://dev.twitter.com/overview/api/upcoming-changes-to-tweets
type ExtendedTweet struct {
FullText string `json:"full_text"`
DisplayTextRange Indices `json:"display_text_range"`
Entities *Entities `json:"entities"`
ExtendedEntities *ExtendedEntity `json:"extended_entities"`
}
// Place represents a Twitter Place / Location
// https://dev.twitter.com/overview/api/places
type Place struct {
Attributes map[string]string `json:"attributes"`
BoundingBox *BoundingBox `json:"bounding_box"`
Country string `json:"country"`
CountryCode string `json:"country_code"`
FullName string `json:"full_name"`
Geometry *BoundingBox `json:"geometry"`
ID string `json:"id"`
Name string `json:"name"`
PlaceType string `json:"place_type"`
Polylines []string `json:"polylines"`
URL string `json:"url"`
}
// BoundingBox represents the bounding coordinates (longitude, latitutde)
// defining the bounds of a box containing a Place entity.
type BoundingBox struct {
Coordinates [][][2]float64 `json:"coordinates"`
Type string `json:"type"`
}
// Coordinates are pairs of longitude and latitude locations.
type Coordinates struct {
Coordinates [2]float64 `json:"coordinates"`
Type string `json:"type"`
}
// TweetIdentifier represents the id by which a Tweet can be identified.
type TweetIdentifier struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
}
// StatusService provides methods for accessing Twitter status API endpoints.
type StatusService struct {
sling *sling.Sling
}
// newStatusService returns a new StatusService.
func newStatusService(sling *sling.Sling) *StatusService {
return &StatusService{
sling: sling.Path("statuses/"),
}
}
// StatusShowParams are the parameters for StatusService.Show
type StatusShowParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
IncludeMyRetweet *bool `url:"include_my_retweet,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Show returns the requested Tweet.
// https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid
func (s *StatusService) Show(id int64, params *StatusShowParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusShowParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusLookupParams are the parameters for StatusService.Lookup
type StatusLookupParams struct {
ID []int64 `url:"id,omitempty,comma"`
TrimUser *bool `url:"trim_user,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
Map *bool `url:"map,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Lookup returns the requested Tweets as a slice. Combines ids from the
// required ids argument and from params.Id.
// https://dev.twitter.com/rest/reference/get/statuses/lookup
func (s *StatusService) Lookup(ids []int64, params *StatusLookupParams) ([]Tweet, *http.Response, error) {
if params == nil {
params = &StatusLookupParams{}
}
params.ID = append(params.ID, ids...)
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("lookup.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// StatusUpdateParams are the parameters for StatusService.Update
type StatusUpdateParams struct {
Status string `url:"status,omitempty"`
InReplyToStatusID int64 `url:"in_reply_to_status_id,omitempty"`
PossiblySensitive *bool `url:"possibly_sensitive,omitempty"`
Lat *float64 `url:"lat,omitempty"`
Long *float64 `url:"long,omitempty"`
PlaceID string `url:"place_id,omitempty"`
DisplayCoordinates *bool `url:"display_coordinates,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
MediaIds []int64 `url:"media_ids,omitempty,comma"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Update updates the user's status, also known as Tweeting.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/update
func (s *StatusService) Update(status string, params *StatusUpdateParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusUpdateParams{}
}
params.Status = status
tweet := new(Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Post("update.json").BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusRetweetParams are the parameters for StatusService.Retweet
type StatusRetweetParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Retweet retweets the Tweet with the given id and returns the original Tweet
// with embedded retweet details.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/retweet/%3Aid
func (s *StatusService) Retweet(id int64, params *StatusRetweetParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusRetweetParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
path := fmt.Sprintf("retweet/%d.json", params.ID)
resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusUnretweetParams are the parameters for StatusService.Unretweet
type StatusUnretweetParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Unretweet unretweets the Tweet with the given id and returns the original Tweet.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/unretweet/%3Aid
func (s *StatusService) Unretweet(id int64, params *StatusUnretweetParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusUnretweetParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
path := fmt.Sprintf("unretweet/%d.json", params.ID)
resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// StatusRetweetsParams are the parameters for StatusService.Retweets
type StatusRetweetsParams struct {
ID int64 `url:"id,omitempty"`
Count int `url:"count,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Retweets returns the most recent retweets of the Tweet with the given id.
// https://dev.twitter.com/rest/reference/get/statuses/retweets/%3Aid
func (s *StatusService) Retweets(id int64, params *StatusRetweetsParams) ([]Tweet, *http.Response, error) {
if params == nil {
params = &StatusRetweetsParams{}
}
params.ID = id
tweets := new([]Tweet)
apiError := new(APIError)
path := fmt.Sprintf("retweets/%d.json", params.ID)
resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// StatusDestroyParams are the parameters for StatusService.Destroy
type StatusDestroyParams struct {
ID int64 `url:"id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// Destroy deletes the Tweet with the given id and returns it if successful.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/post/statuses/destroy/%3Aid
func (s *StatusService) Destroy(id int64, params *StatusDestroyParams) (*Tweet, *http.Response, error) {
if params == nil {
params = &StatusDestroyParams{}
}
params.ID = id
tweet := new(Tweet)
apiError := new(APIError)
path := fmt.Sprintf("destroy/%d.json", params.ID)
resp, err := s.sling.New().Post(path).BodyForm(params).Receive(tweet, apiError)
return tweet, resp, relevantError(err, *apiError)
}
// OEmbedTweet represents a Tweet in oEmbed format.
type OEmbedTweet struct {
URL string `json:"url"`
ProviderURL string `json:"provider_url"`
ProviderName string `json:"provider_name"`
AuthorName string `json:"author_name"`
Version string `json:"version"`
AuthorURL string `json:"author_url"`
Type string `json:"type"`
HTML string `json:"html"`
Height int64 `json:"height"`
Width int64 `json:"width"`
CacheAge string `json:"cache_age"`
}
// StatusOEmbedParams are the parameters for StatusService.OEmbed
type StatusOEmbedParams struct {
ID int64 `url:"id,omitempty"`
URL string `url:"url,omitempty"`
Align string `url:"align,omitempty"`
MaxWidth int64 `url:"maxwidth,omitempty"`
HideMedia *bool `url:"hide_media,omitempty"`
HideThread *bool `url:"hide_media,omitempty"`
OmitScript *bool `url:"hide_media,omitempty"`
WidgetType string `url:"widget_type,omitempty"`
HideTweet *bool `url:"hide_tweet,omitempty"`
}
// OEmbed returns the requested Tweet in oEmbed format.
// https://dev.twitter.com/rest/reference/get/statuses/oembed
func (s *StatusService) OEmbed(params *StatusOEmbedParams) (*OEmbedTweet, *http.Response, error) {
oEmbedTweet := new(OEmbedTweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("oembed.json").QueryStruct(params).Receive(oEmbedTweet, apiError)
return oEmbedTweet, resp, relevantError(err, *apiError)
}

View File

@@ -1,275 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStatusService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "589488862814076930", "include_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"user": {"screen_name": "dghubble"}, "text": ".@audreyr use a DONTREADME file if you really want people to read it :P"}`)
})
client := NewClient(httpClient)
params := &StatusShowParams{ID: 5441, IncludeEntities: Bool(false)}
tweet, _, err := client.Statuses.Show(589488862814076930, params)
expected := &Tweet{User: &User{ScreenName: "dghubble"}, Text: ".@audreyr use a DONTREADME file if you really want people to read it :P"}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_ShowHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/show.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"id": "589488862814076930"}, r)
})
client := NewClient(httpClient)
client.Statuses.Show(589488862814076930, nil)
}
func TestStatusService_Lookup(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "20,573893817000140800", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"id": 20, "text": "just setting up my twttr"}, {"id": 573893817000140800, "text": "Don't get lost #PaxEast2015"}]`)
})
client := NewClient(httpClient)
params := &StatusLookupParams{ID: []int64{20}, TrimUser: Bool(true)}
tweets, _, err := client.Statuses.Lookup([]int64{573893817000140800}, params)
expected := []Tweet{Tweet{ID: 20, Text: "just setting up my twttr"}, Tweet{ID: 573893817000140800, Text: "Don't get lost #PaxEast2015"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestStatusService_LookupHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"id": "20,573893817000140800"}, r)
})
client := NewClient(httpClient)
client.Statuses.Lookup([]int64{20, 573893817000140800}, nil)
}
func TestStatusService_Update(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"status": "very informative tweet", "media_ids": "123456789,987654321", "lat": "37.826706", "long": "-122.42219"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630845953, "text": "very informative tweet"}`)
})
client := NewClient(httpClient)
params := &StatusUpdateParams{MediaIds: []int64{123456789, 987654321}, Lat: Float(37.826706), Long: Float(-122.422190)}
tweet, _, err := client.Statuses.Update("very informative tweet", params)
expected := &Tweet{ID: 581980947630845953, Text: "very informative tweet"}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_UpdateHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"status": "very informative tweet"}, r)
})
client := NewClient(httpClient)
client.Statuses.Update("very informative tweet", nil)
}
func TestStatusService_APIError(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/update.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"status": "very informative tweet"}, r)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(403)
fmt.Fprintf(w, `{"errors": [{"message": "Status is a duplicate", "code": 187}]}`)
})
client := NewClient(httpClient)
_, _, err := client.Statuses.Update("very informative tweet", nil)
expected := APIError{
Errors: []ErrorDetail{
ErrorDetail{Message: "Status is a duplicate", Code: 187},
},
}
if assert.Error(t, err) {
assert.Equal(t, expected, err)
}
}
func TestStatusService_HTTPError(t *testing.T) {
httpClient, _, server := testServer()
server.Close()
client := NewClient(httpClient)
_, _, err := client.Statuses.Update("very informative tweet", nil)
if err == nil || !strings.Contains(err.Error(), "connection refused") {
t.Errorf("Statuses.Update error expected connection refused, got: \n %+v", err)
}
}
func TestStatusService_Retweet(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweet/20.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"id": "20", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630202020, "text": "RT @jack: just setting up my twttr", "retweeted_status": {"id": 20, "text": "just setting up my twttr"}}`)
})
client := NewClient(httpClient)
params := &StatusRetweetParams{TrimUser: Bool(true)}
tweet, _, err := client.Statuses.Retweet(20, params)
expected := &Tweet{ID: 581980947630202020, Text: "RT @jack: just setting up my twttr", RetweetedStatus: &Tweet{ID: 20, Text: "just setting up my twttr"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_RetweetHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweet/20.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"id": "20"}, r)
})
client := NewClient(httpClient)
client.Statuses.Retweet(20, nil)
}
func TestStatusService_Unretweet(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/unretweet/20.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"id": "20", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 581980947630202020, "text":"RT @jack: just setting up my twttr", "retweeted_status": {"id": 20, "text": "just setting up my twttr"}}`)
})
client := NewClient(httpClient)
params := &StatusUnretweetParams{TrimUser: Bool(true)}
tweet, _, err := client.Statuses.Unretweet(20, params)
expected := &Tweet{ID: 581980947630202020, Text: "RT @jack: just setting up my twttr", RetweetedStatus: &Tweet{ID: 20, Text: "just setting up my twttr"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_Retweets(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweets/20.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "20", "count": "2"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "RT @jack: just setting up my twttr"}, {"text": "RT @jack: just setting up my twttr"}]`)
})
client := NewClient(httpClient)
params := &StatusRetweetsParams{Count: 2}
retweets, _, err := client.Statuses.Retweets(20, params)
expected := []Tweet{Tweet{Text: "RT @jack: just setting up my twttr"}, Tweet{Text: "RT @jack: just setting up my twttr"}}
assert.Nil(t, err)
assert.Equal(t, expected, retweets)
}
func TestStatusService_RetweetsHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweets/20.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"id": "20"}, r)
})
client := NewClient(httpClient)
client.Statuses.Retweets(20, nil)
}
func TestStatusService_Destroy(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/destroy/40.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{}, r)
assertPostForm(t, map[string]string{"id": "40", "trim_user": "true"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"id": 40, "text": "wishing I had another sammich"}`)
})
client := NewClient(httpClient)
params := &StatusDestroyParams{TrimUser: Bool(true)}
tweet, _, err := client.Statuses.Destroy(40, params)
// feed Biz Stone a sammich, he deletes sammich Tweet
expected := &Tweet{ID: 40, Text: "wishing I had another sammich"}
assert.Nil(t, err)
assert.Equal(t, expected, tweet)
}
func TestStatusService_DestroyHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/destroy/40.json", func(w http.ResponseWriter, r *http.Request) {
assertPostForm(t, map[string]string{"id": "40"}, r)
})
client := NewClient(httpClient)
client.Statuses.Destroy(40, nil)
}
func TestStatusService_OEmbed(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/oembed.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "691076766878691329", "maxwidth": "400", "hide_media": "true"}, r)
w.Header().Set("Content-Type", "application/json")
// abbreviated oEmbed response
fmt.Fprintf(w, `{"url": "https://twitter.com/dghubble/statuses/691076766878691329", "width": 400, "html": "<blockquote></blockquote>"}`)
})
client := NewClient(httpClient)
params := &StatusOEmbedParams{
ID: 691076766878691329,
MaxWidth: 400,
HideMedia: Bool(true),
}
oembed, _, err := client.Statuses.OEmbed(params)
expected := &OEmbedTweet{
URL: "https://twitter.com/dghubble/statuses/691076766878691329",
Width: 400,
HTML: "<blockquote></blockquote>",
}
assert.Nil(t, err)
assert.Equal(t, expected, oembed)
}

View File

@@ -1,110 +0,0 @@
package twitter
// StatusDeletion indicates that a given Tweet has been deleted.
// https://dev.twitter.com/streaming/overview/messages-types#status_deletion_notices_delete
type StatusDeletion struct {
ID int64 `json:"id"`
IDStr string `json:"id_str"`
UserID int64 `json:"user_id"`
UserIDStr string `json:"user_id_str"`
}
type statusDeletionNotice struct {
Delete struct {
StatusDeletion *StatusDeletion `json:"status"`
} `json:"delete"`
}
// LocationDeletion indicates geolocation data must be stripped from a range
// of Tweets.
// https://dev.twitter.com/streaming/overview/messages-types#Location_deletion_notices_scrub_geo
type LocationDeletion struct {
UserID int64 `json:"user_id"`
UserIDStr string `json:"user_id_str"`
UpToStatusID int64 `json:"up_to_status_id"`
UpToStatusIDStr string `json:"up_to_status_id_str"`
}
type locationDeletionNotice struct {
ScrubGeo *LocationDeletion `json:"scrub_geo"`
}
// StreamLimit indicates a stream matched more statuses than its rate limit
// allowed. The track number is the number of undelivered matches.
// https://dev.twitter.com/streaming/overview/messages-types#limit_notices
type StreamLimit struct {
Track int64 `json:"track"`
}
type streamLimitNotice struct {
Limit *StreamLimit `json:"limit"`
}
// StatusWithheld indicates a Tweet with the given ID, belonging to UserId,
// has been withheld in certain countries.
// https://dev.twitter.com/streaming/overview/messages-types#withheld_content_notices
type StatusWithheld struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
WithheldInCountries []string `json:"withheld_in_countries"`
}
type statusWithheldNotice struct {
StatusWithheld *StatusWithheld `json:"status_withheld"`
}
// UserWithheld indicates a User with the given ID has been withheld in
// certain countries.
// https://dev.twitter.com/streaming/overview/messages-types#withheld_content_notices
type UserWithheld struct {
ID int64 `json:"id"`
WithheldInCountries []string `json:"withheld_in_countries"`
}
type userWithheldNotice struct {
UserWithheld *UserWithheld `json:"user_withheld"`
}
// StreamDisconnect indicates the stream has been shutdown for some reason.
// https://dev.twitter.com/streaming/overview/messages-types#disconnect_messages
type StreamDisconnect struct {
Code int64 `json:"code"`
StreamName string `json:"stream_name"`
Reason string `json:"reason"`
}
type streamDisconnectNotice struct {
StreamDisconnect *StreamDisconnect `json:"disconnect"`
}
// StallWarning indicates the client is falling behind in the stream.
// https://dev.twitter.com/streaming/overview/messages-types#stall_warnings
type StallWarning struct {
Code string `json:"code"`
Message string `json:"message"`
PercentFull int `json:"percent_full"`
}
type stallWarningNotice struct {
StallWarning *StallWarning `json:"warning"`
}
// FriendsList is a list of some of a user's friends.
// https://dev.twitter.com/streaming/overview/messages-types#friends_list_friends
type FriendsList struct {
Friends []int64 `json:"friends"`
}
type directMessageNotice struct {
DirectMessage *DirectMessage `json:"direct_message"`
}
// Event is a non-Tweet notification message (e.g. like, retweet, follow).
// https://dev.twitter.com/streaming/overview/messages-types#Events_event
type Event struct {
Event string `json:"event"`
CreatedAt string `json:"created_at"`
Target *User `json:"target"`
Source *User `json:"source"`
// TODO: add List or deprecate it
TargetObject *Tweet `json:"target_object"`
}

View File

@@ -1,95 +0,0 @@
package twitter
import (
"bufio"
"bytes"
"io"
"time"
)
// stopped returns true if the done channel receives, false otherwise.
func stopped(done <-chan struct{}) bool {
select {
case <-done:
return true
default:
return false
}
}
// sleepOrDone pauses the current goroutine until the done channel receives
// or until at least the duration d has elapsed, whichever comes first. This
// is similar to time.Sleep(d), except it can be interrupted.
func sleepOrDone(d time.Duration, done <-chan struct{}) {
sleep := time.NewTimer(d)
defer sleep.Stop()
select {
case <-sleep.C:
return
case <-done:
return
}
}
// streamResponseBodyReader is a buffered reader for Twitter stream response
// body. It can scan the arbitrary length of response body unlike bufio.Scanner.
type streamResponseBodyReader struct {
reader *bufio.Reader
buf bytes.Buffer
}
// newStreamResponseBodyReader returns an instance of streamResponseBodyReader
// for the given Twitter stream response body.
func newStreamResponseBodyReader(body io.Reader) *streamResponseBodyReader {
return &streamResponseBodyReader{reader: bufio.NewReader(body)}
}
// readNext reads Twitter stream response body and returns the next stream
// content if exists. Returns io.EOF error if we reached the end of the stream
// and there's no more message to read.
func (r *streamResponseBodyReader) readNext() ([]byte, error) {
// Discard all the bytes from buf and continue to use the allocated memory
// space for reading the next message.
r.buf.Truncate(0)
for {
// Twitter stream messages are separated with "\r\n", and a valid
// message may sometimes contain '\n' in the middle.
// bufio.Reader.Read() can accept one byte delimiter only, so we need to
// first break out each line on '\n' and then check whether the line ends
// with "\r\n" to find message boundaries.
// https://dev.twitter.com/streaming/overview/processing
line, err := r.reader.ReadBytes('\n')
// Non-EOF error should be propagated to callers immediately.
if err != nil && err != io.EOF {
return nil, err
}
// EOF error means that we reached the end of the stream body before finding
// delimiter '\n'. If "line" is empty, it means the reader didn't read any
// data from the stream before reaching EOF and there's nothing to append to
// buf.
if err == io.EOF && len(line) == 0 {
// if buf has no data, propagate io.EOF to callers and let them know that
// we've finished processing the stream.
if r.buf.Len() == 0 {
return nil, err
}
// Otherwise, we still have a remaining stream message to return.
break
}
// If the line ends with "\r\n", it's the end of one stream message data.
if bytes.HasSuffix(line, []byte("\r\n")) {
// reader.ReadBytes() returns a slice including the delimiter itself, so
// we need to trim '\n' as well as '\r' from the end of the slice.
r.buf.Write(bytes.TrimRight(line, "\r\n"))
break
}
// Otherwise, the line is not the end of a stream message, so we append
// the line to buf and continue to scan lines.
r.buf.Write(line)
}
// Get the stream message bytes from buf. Not that Bytes() won't mark the
// returned data as "read", and we need to explicitly call Truncate(0) to
// discard from buf before writing the next stream message to buf.
return r.buf.Bytes(), nil
}

View File

@@ -1,111 +0,0 @@
package twitter
import (
"bufio"
"bytes"
"io"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestStopped(t *testing.T) {
done := make(chan struct{})
assert.False(t, stopped(done))
close(done)
assert.True(t, stopped(done))
}
func TestSleepOrDone_Sleep(t *testing.T) {
wait := time.Nanosecond * 20
done := make(chan struct{})
completed := make(chan struct{})
go func() {
sleepOrDone(wait, done)
close(completed)
}()
// wait for goroutine SleepOrDone to sleep
assertDone(t, completed, defaultTestTimeout)
}
func TestSleepOrDone_Done(t *testing.T) {
wait := time.Second * 5
done := make(chan struct{})
completed := make(chan struct{})
go func() {
sleepOrDone(wait, done)
close(completed)
}()
// close done, interrupting SleepOrDone
close(done)
// assert that SleepOrDone exited, closing completed
assertDone(t, completed, defaultTestTimeout)
}
func TestStreamResponseBodyReader(t *testing.T) {
cases := []struct {
in []byte
want [][]byte
}{
{
in: []byte("foo\r\nbar\r\n"),
want: [][]byte{
[]byte("foo"),
[]byte("bar"),
},
},
{
in: []byte("foo\nbar\r\n"),
want: [][]byte{
[]byte("foo\nbar"),
},
},
{
in: []byte("foo\r\n\r\n"),
want: [][]byte{
[]byte("foo"),
[]byte(""),
},
},
{
in: []byte("foo\r\nbar"),
want: [][]byte{
[]byte("foo"),
[]byte("bar"),
},
},
{
// Message length is more than bufio.MaxScanTokenSize, which can't be
// parsed by bufio.Scanner with default buffer size.
in: []byte(strings.Repeat("X", bufio.MaxScanTokenSize+1) + "\r\n"),
want: [][]byte{
[]byte(strings.Repeat("X", bufio.MaxScanTokenSize+1)),
},
},
}
for _, c := range cases {
body := bytes.NewReader(c.in)
reader := newStreamResponseBodyReader(body)
for i, want := range c.want {
data, err := reader.readNext()
if err != nil {
t.Errorf("reader(%q).readNext() * %d: err == %q, want nil", c.in, i, err)
}
if !bytes.Equal(data, want) {
t.Errorf("reader(%q).readNext() * %d: data == %q, want %q", c.in, i, data, want)
}
}
data, err := reader.readNext()
if err != io.EOF {
t.Errorf("reader(%q).readNext() * %d: err == %q, want io.EOF", c.in, len(c.want), err)
}
if len(data) != 0 {
t.Errorf("reader(%q).readNext() * %d: data == %q, want \"\"", c.in, len(c.want), data)
}
}
}

View File

@@ -1,323 +0,0 @@
package twitter
import (
"encoding/json"
"io"
"net/http"
"sync"
"time"
"github.com/cenkalti/backoff"
"github.com/dghubble/sling"
)
const (
userAgent = "go-twitter v0.1"
publicStream = "https://stream.twitter.com/1.1/"
userStream = "https://userstream.twitter.com/1.1/"
siteStream = "https://sitestream.twitter.com/1.1/"
)
// StreamService provides methods for accessing the Twitter Streaming API.
type StreamService struct {
client *http.Client
public *sling.Sling
user *sling.Sling
site *sling.Sling
}
// newStreamService returns a new StreamService.
func newStreamService(client *http.Client, sling *sling.Sling) *StreamService {
sling.Set("User-Agent", userAgent)
return &StreamService{
client: client,
public: sling.New().Base(publicStream).Path("statuses/"),
user: sling.New().Base(userStream),
site: sling.New().Base(siteStream),
}
}
// StreamFilterParams are parameters for StreamService.Filter.
type StreamFilterParams struct {
FilterLevel string `url:"filter_level,omitempty"`
Follow []string `url:"follow,omitempty,comma"`
Language []string `url:"language,omitempty,comma"`
Locations []string `url:"locations,omitempty,comma"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
Track []string `url:"track,omitempty,comma"`
}
// Filter returns messages that match one or more filter predicates.
// https://dev.twitter.com/streaming/reference/post/statuses/filter
func (srv *StreamService) Filter(params *StreamFilterParams) (*Stream, error) {
req, err := srv.public.New().Post("filter.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamSampleParams are the parameters for StreamService.Sample.
type StreamSampleParams struct {
StallWarnings *bool `url:"stall_warnings,omitempty"`
Language []string `url:"language,omitempty,comma"`
}
// Sample returns a small sample of public stream messages.
// https://dev.twitter.com/streaming/reference/get/statuses/sample
func (srv *StreamService) Sample(params *StreamSampleParams) (*Stream, error) {
req, err := srv.public.New().Get("sample.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamUserParams are the parameters for StreamService.User.
type StreamUserParams struct {
FilterLevel string `url:"filter_level,omitempty"`
Language []string `url:"language,omitempty,comma"`
Locations []string `url:"locations,omitempty,comma"`
Replies string `url:"replies,omitempty"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
Track []string `url:"track,omitempty,comma"`
With string `url:"with,omitempty"`
}
// User returns a stream of messages specific to the authenticated User.
// https://dev.twitter.com/streaming/reference/get/user
func (srv *StreamService) User(params *StreamUserParams) (*Stream, error) {
req, err := srv.user.New().Get("user.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamSiteParams are the parameters for StreamService.Site.
type StreamSiteParams struct {
FilterLevel string `url:"filter_level,omitempty"`
Follow []string `url:"follow,omitempty,comma"`
Language []string `url:"language,omitempty,comma"`
Replies string `url:"replies,omitempty"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
With string `url:"with,omitempty"`
}
// Site returns messages for a set of users.
// Requires special permission to access.
// https://dev.twitter.com/streaming/reference/get/site
func (srv *StreamService) Site(params *StreamSiteParams) (*Stream, error) {
req, err := srv.site.New().Get("site.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// StreamFirehoseParams are the parameters for StreamService.Firehose.
type StreamFirehoseParams struct {
Count int `url:"count,omitempty"`
FilterLevel string `url:"filter_level,omitempty"`
Language []string `url:"language,omitempty,comma"`
StallWarnings *bool `url:"stall_warnings,omitempty"`
}
// Firehose returns all public messages and statuses.
// Requires special permission to access.
// https://dev.twitter.com/streaming/reference/get/statuses/firehose
func (srv *StreamService) Firehose(params *StreamFirehoseParams) (*Stream, error) {
req, err := srv.public.New().Get("firehose.json").QueryStruct(params).Request()
if err != nil {
return nil, err
}
return newStream(srv.client, req), nil
}
// Stream maintains a connection to the Twitter Streaming API, receives
// messages from the streaming response, and sends them on the Messages
// channel from a goroutine. The stream goroutine stops itself if an EOF is
// reached or retry errors occur, also closing the Messages channel.
//
// The client must Stop() the stream when finished receiving, which will
// wait until the stream is properly stopped.
type Stream struct {
client *http.Client
Messages chan interface{}
done chan struct{}
group *sync.WaitGroup
body io.Closer
}
// newStream creates a Stream and starts a goroutine to retry connecting and
// receive from a stream response. The goroutine may stop due to retry errors
// or be stopped by calling Stop() on the stream.
func newStream(client *http.Client, req *http.Request) *Stream {
s := &Stream{
client: client,
Messages: make(chan interface{}),
done: make(chan struct{}),
group: &sync.WaitGroup{},
}
s.group.Add(1)
go s.retry(req, newExponentialBackOff(), newAggressiveExponentialBackOff())
return s
}
// Stop signals retry and receiver to stop, closes the Messages channel, and
// blocks until done.
func (s *Stream) Stop() {
close(s.done)
// Scanner does not have a Stop() or take a done channel, so for low volume
// streams Scan() blocks until the next keep-alive. Close the resp.Body to
// escape and stop the stream in a timely fashion.
if s.body != nil {
s.body.Close()
}
// block until the retry goroutine stops
s.group.Wait()
}
// retry retries making the given http.Request and receiving the response
// according to the Twitter backoff policies. Callers should invoke in a
// goroutine since backoffs sleep between retries.
// https://dev.twitter.com/streaming/overview/connecting
func (s *Stream) retry(req *http.Request, expBackOff backoff.BackOff, aggExpBackOff backoff.BackOff) {
// close Messages channel and decrement the wait group counter
defer close(s.Messages)
defer s.group.Done()
var wait time.Duration
for !stopped(s.done) {
resp, err := s.client.Do(req)
if err != nil {
// stop retrying for HTTP protocol errors
s.Messages <- err
return
}
// when err is nil, resp contains a non-nil Body which must be closed
defer resp.Body.Close()
s.body = resp.Body
switch resp.StatusCode {
case 200:
// receive stream response Body, handles closing
s.receive(resp.Body)
expBackOff.Reset()
aggExpBackOff.Reset()
case 503:
// exponential backoff
wait = expBackOff.NextBackOff()
case 420, 429:
// aggressive exponential backoff
wait = aggExpBackOff.NextBackOff()
default:
// stop retrying for other response codes
resp.Body.Close()
return
}
// close response before each retry
resp.Body.Close()
if wait == backoff.Stop {
return
}
sleepOrDone(wait, s.done)
}
}
// receive scans a stream response body, JSON decodes tokens to messages, and
// sends messages to the Messages channel. Receiving continues until an EOF,
// scan error, or the done channel is closed.
func (s *Stream) receive(body io.Reader) {
reader := newStreamResponseBodyReader(body)
for !stopped(s.done) {
data, err := reader.readNext()
if err != nil {
return
}
if len(data) == 0 {
// empty keep-alive
continue
}
select {
// send messages, data, or errors
case s.Messages <- getMessage(data):
continue
// allow client to Stop(), even if not receiving
case <-s.done:
return
}
}
}
// getMessage unmarshals the token and returns a message struct, if the type
// can be determined. Otherwise, returns the token unmarshalled into a data
// map[string]interface{} or the unmarshal error.
func getMessage(token []byte) interface{} {
var data map[string]interface{}
// unmarshal JSON encoded token into a map for
err := json.Unmarshal(token, &data)
if err != nil {
return err
}
return decodeMessage(token, data)
}
// decodeMessage determines the message type from known data keys, allocates
// at most one message struct, and JSON decodes the token into the message.
// Returns the message struct or the data map if the message type could not be
// determined.
func decodeMessage(token []byte, data map[string]interface{}) interface{} {
if hasPath(data, "retweet_count") {
tweet := new(Tweet)
json.Unmarshal(token, tweet)
return tweet
} else if hasPath(data, "direct_message") {
notice := new(directMessageNotice)
json.Unmarshal(token, notice)
return notice.DirectMessage
} else if hasPath(data, "delete") {
notice := new(statusDeletionNotice)
json.Unmarshal(token, notice)
return notice.Delete.StatusDeletion
} else if hasPath(data, "scrub_geo") {
notice := new(locationDeletionNotice)
json.Unmarshal(token, notice)
return notice.ScrubGeo
} else if hasPath(data, "limit") {
notice := new(streamLimitNotice)
json.Unmarshal(token, notice)
return notice.Limit
} else if hasPath(data, "status_withheld") {
notice := new(statusWithheldNotice)
json.Unmarshal(token, notice)
return notice.StatusWithheld
} else if hasPath(data, "user_withheld") {
notice := new(userWithheldNotice)
json.Unmarshal(token, notice)
return notice.UserWithheld
} else if hasPath(data, "disconnect") {
notice := new(streamDisconnectNotice)
json.Unmarshal(token, notice)
return notice.StreamDisconnect
} else if hasPath(data, "warning") {
notice := new(stallWarningNotice)
json.Unmarshal(token, notice)
return notice.StallWarning
} else if hasPath(data, "friends") {
friendsList := new(FriendsList)
json.Unmarshal(token, friendsList)
return friendsList
} else if hasPath(data, "event") {
event := new(Event)
json.Unmarshal(token, event)
return event
}
// message type unknown, return the data map[string]interface{}
return data
}
// hasPath returns true if the map contains the given key, false otherwise.
func hasPath(data map[string]interface{}, key string) bool {
_, ok := data[key]
return ok
}

View File

@@ -1,393 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStream_MessageJSONError(t *testing.T) {
badJSON := []byte(`{`)
msg := getMessage(badJSON)
assert.EqualError(t, msg.(error), "unexpected end of JSON input")
}
func TestStream_GetMessageTweet(t *testing.T) {
msgJSON := []byte(`{"id": 20, "text": "just setting up my twttr", "retweet_count": "68535"}`)
msg := getMessage(msgJSON)
assert.IsType(t, &Tweet{}, msg)
}
func TestStream_GetMessageDirectMessage(t *testing.T) {
msgJSON := []byte(`{"direct_message": {"id": 666024290140217347}}`)
msg := getMessage(msgJSON)
assert.IsType(t, &DirectMessage{}, msg)
}
func TestStream_GetMessageDelete(t *testing.T) {
msgJSON := []byte(`{"delete": { "id": 20}}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StatusDeletion{}, msg)
}
func TestStream_GetMessageLocationDeletion(t *testing.T) {
msgJSON := []byte(`{"scrub_geo": { "up_to_status_id": 20}}`)
msg := getMessage(msgJSON)
assert.IsType(t, &LocationDeletion{}, msg)
}
func TestStream_GetMessageStreamLimit(t *testing.T) {
msgJSON := []byte(`{"limit": { "track": 10 }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StreamLimit{}, msg)
}
func TestStream_StatusWithheld(t *testing.T) {
msgJSON := []byte(`{"status_withheld": { "id": 20, "user_id": 12, "withheld_in_countries":["USA", "China"] }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StatusWithheld{}, msg)
}
func TestStream_UserWithheld(t *testing.T) {
msgJSON := []byte(`{"user_withheld": { "id": 12, "withheld_in_countries":["USA", "China"] }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &UserWithheld{}, msg)
}
func TestStream_StreamDisconnect(t *testing.T) {
msgJSON := []byte(`{"disconnect": { "code": "420", "stream_name": "streaming stuff", "reason": "too many connections" }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StreamDisconnect{}, msg)
}
func TestStream_StallWarning(t *testing.T) {
msgJSON := []byte(`{"warning": { "code": "420", "percent_full": 90, "message": "a lot of messages" }}`)
msg := getMessage(msgJSON)
assert.IsType(t, &StallWarning{}, msg)
}
func TestStream_FriendsList(t *testing.T) {
msgJSON := []byte(`{"friends": [666024290140217347, 666024290140217349, 666024290140217342]}`)
msg := getMessage(msgJSON)
assert.IsType(t, &FriendsList{}, msg)
}
func TestStream_Event(t *testing.T) {
msgJSON := []byte(`{"event": "block", "target": {"name": "XKCD Comic", "favourites_count": 2}, "source": {"name": "XKCD Comic2", "favourites_count": 3}, "created_at": "Sat Sep 4 16:10:54 +0000 2010"}`)
msg := getMessage(msgJSON)
assert.IsType(t, &Event{}, msg)
}
func TestStream_Unknown(t *testing.T) {
msgJSON := []byte(`{"unknown_data": {"new_twitter_type":"unexpected"}}`)
msg := getMessage(msgJSON)
assert.IsType(t, map[string]interface{}{}, msg)
}
func TestStream_Filter(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/statuses/filter.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "POST", r)
assertQuery(t, map[string]string{"track": "gophercon,golang"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamFilterParams := &StreamFilterParams{
Track: []string{"gophercon", "golang"},
}
stream, err := client.Streams.Filter(streamFilterParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_Sample(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/statuses/sample.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"stall_warnings": "true"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamSampleParams := &StreamSampleParams{
StallWarnings: Bool(true),
}
stream, err := client.Streams.Sample(streamSampleParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_User(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/user.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"stall_warnings": "true", "with": "followings"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w, `{"friends": [666024290140217347, 666024290140217349, 666024290140217342]}`+"\r\n"+"\r\n")
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamUserParams := &StreamUserParams{
StallWarnings: Bool(true),
With: "followings",
}
stream, err := client.Streams.User(streamUserParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 1, friendsList: 1}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_User_TooManyFriends(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/user.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"stall_warnings": "true", "with": "followings"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
// The first friend list message is more than bufio.MaxScanTokenSize (65536) bytes
friendsList := "[" + strings.Repeat("1234567890, ", 7000) + "1234567890]"
fmt.Fprintf(w, `{"friends": %s}`+"\r\n"+"\r\n", friendsList)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamUserParams := &StreamUserParams{
StallWarnings: Bool(true),
With: "followings",
}
stream, err := client.Streams.User(streamUserParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 1, friendsList: 1}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_Site(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/site.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"follow": "666024290140217347,666024290140217349"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamSiteParams := &StreamSiteParams{
Follow: []string{"666024290140217347", "666024290140217349"},
}
stream, err := client.Streams.Site(streamSiteParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStream_PublicFirehose(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/1.1/statuses/firehose.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"count": "100"}, r)
switch reqCount {
case 0:
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Transfer-Encoding", "chunked")
fmt.Fprintf(w,
`{"text": "Gophercon talks!"}`+"\r\n"+
`{"text": "Gophercon super talks!"}`+"\r\n",
)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
counts := &counter{}
demux := newCounterDemux(counts)
client := NewClient(httpClient)
streamFirehoseParams := &StreamFirehoseParams{
Count: 100,
}
stream, err := client.Streams.Firehose(streamFirehoseParams)
// assert that the expected messages are received
assert.NoError(t, err)
defer stream.Stop()
for message := range stream.Messages {
demux.Handle(message)
}
expectedCounts := &counter{all: 2, other: 2}
assert.Equal(t, expectedCounts, counts)
}
func TestStreamRetry_ExponentialBackoff(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch reqCount {
case 0:
http.Error(w, "Service Unavailable", 503)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
stream := &Stream{
client: httpClient,
Messages: make(chan interface{}),
done: make(chan struct{}),
group: &sync.WaitGroup{},
}
stream.group.Add(1)
req, _ := http.NewRequest("GET", "http://example.com/", nil)
expBackoff := &BackOffRecorder{}
// receive messages and throw them away
go NewSwitchDemux().HandleChan(stream.Messages)
stream.retry(req, expBackoff, nil)
defer stream.Stop()
// assert exponential backoff in response to 503
assert.Equal(t, 1, expBackoff.Count)
}
func TestStreamRetry_AggressiveBackoff(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
reqCount := 0
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch reqCount {
case 0:
http.Error(w, "Enhance Your Calm", 420)
case 1:
http.Error(w, "Too Many Requests", 429)
default:
// Only allow first request
http.Error(w, "Stream API not available!", 130)
}
reqCount++
})
stream := &Stream{
client: httpClient,
Messages: make(chan interface{}),
done: make(chan struct{}),
group: &sync.WaitGroup{},
}
stream.group.Add(1)
req, _ := http.NewRequest("GET", "http://example.com/", nil)
aggExpBackoff := &BackOffRecorder{}
// receive messages and throw them away
go NewSwitchDemux().HandleChan(stream.Messages)
stream.retry(req, nil, aggExpBackoff)
defer stream.Stop()
// assert aggressive exponential backoff in response to 420 and 429
assert.Equal(t, 2, aggExpBackoff.Count)
}

View File

@@ -1,109 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// TimelineService provides methods for accessing Twitter status timeline
// API endpoints.
type TimelineService struct {
sling *sling.Sling
}
// newTimelineService returns a new TimelineService.
func newTimelineService(sling *sling.Sling) *TimelineService {
return &TimelineService{
sling: sling.Path("statuses/"),
}
}
// UserTimelineParams are the parameters for TimelineService.UserTimeline.
type UserTimelineParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
ExcludeReplies *bool `url:"exclude_replies,omitempty"`
IncludeRetweets *bool `url:"include_rts,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// UserTimeline returns recent Tweets from the specified user.
// https://dev.twitter.com/rest/reference/get/statuses/user_timeline
func (s *TimelineService) UserTimeline(params *UserTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("user_timeline.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// HomeTimelineParams are the parameters for TimelineService.HomeTimeline.
type HomeTimelineParams struct {
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
ExcludeReplies *bool `url:"exclude_replies,omitempty"`
ContributorDetails *bool `url:"contributor_details,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// HomeTimeline returns recent Tweets and retweets from the user and those
// users they follow.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/statuses/home_timeline
func (s *TimelineService) HomeTimeline(params *HomeTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("home_timeline.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// MentionTimelineParams are the parameters for TimelineService.MentionTimeline.
type MentionTimelineParams struct {
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
ContributorDetails *bool `url:"contributor_details,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// MentionTimeline returns recent Tweet mentions of the authenticated user.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/statuses/mentions_timeline
func (s *TimelineService) MentionTimeline(params *MentionTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("mentions_timeline.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}
// RetweetsOfMeTimelineParams are the parameters for
// TimelineService.RetweetsOfMeTimeline.
type RetweetsOfMeTimelineParams struct {
Count int `url:"count,omitempty"`
SinceID int64 `url:"since_id,omitempty"`
MaxID int64 `url:"max_id,omitempty"`
TrimUser *bool `url:"trim_user,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"`
IncludeUserEntities *bool `url:"include_user_entities"`
TweetMode string `url:"tweet_mode,omitempty"`
}
// RetweetsOfMeTimeline returns the most recent Tweets by the authenticated
// user that have been retweeted by others.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me
func (s *TimelineService) RetweetsOfMeTimeline(params *RetweetsOfMeTimelineParams) ([]Tweet, *http.Response, error) {
tweets := new([]Tweet)
apiError := new(APIError)
resp, err := s.sling.New().Get("retweets_of_me.json").QueryStruct(params).Receive(tweets, apiError)
return *tweets, resp, relevantError(err, *apiError)
}

View File

@@ -1,81 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTimelineService_UserTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/user_timeline.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "113419064", "trim_user": "true", "include_rts": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "Gophercon talks!"}, {"text": "Why gophers are so adorable"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.UserTimeline(&UserTimelineParams{UserID: 113419064, TrimUser: Bool(true), IncludeRetweets: Bool(false)})
expected := []Tweet{Tweet{Text: "Gophercon talks!"}, Tweet{Text: "Why gophers are so adorable"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestTimelineService_HomeTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/home_timeline.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"since_id": "589147592367431680", "exclude_replies": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "Live on #Periscope"}, {"text": "Clickbait journalism"}, {"text": "Useful announcement"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.HomeTimeline(&HomeTimelineParams{SinceID: 589147592367431680, ExcludeReplies: Bool(false)})
expected := []Tweet{Tweet{Text: "Live on #Periscope"}, Tweet{Text: "Clickbait journalism"}, Tweet{Text: "Useful announcement"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestTimelineService_MentionTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/mentions_timeline.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"count": "20", "include_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "@dghubble can I get verified?"}, {"text": "@dghubble why are gophers so great?"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.MentionTimeline(&MentionTimelineParams{Count: 20, IncludeEntities: Bool(false)})
expected := []Tweet{Tweet{Text: "@dghubble can I get verified?"}, Tweet{Text: "@dghubble why are gophers so great?"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}
func TestTimelineService_RetweetsOfMeTimeline(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/statuses/retweets_of_me.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"trim_user": "false", "include_user_entities": "false"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"text": "RT Twitter UK edition"}, {"text": "RT Triply-replicated Gophers"}]`)
})
client := NewClient(httpClient)
tweets, _, err := client.Timelines.RetweetsOfMeTimeline(&RetweetsOfMeTimelineParams{TrimUser: Bool(false), IncludeUserEntities: Bool(false)})
expected := []Tweet{Tweet{Text: "RT Twitter UK edition"}, Tweet{Text: "RT Triply-replicated Gophers"}}
assert.Nil(t, err)
assert.Equal(t, expected, tweets)
}

View File

@@ -1,102 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// TrendsService provides methods for accessing Twitter trends API endpoints.
type TrendsService struct {
sling *sling.Sling
}
// newTrendsService returns a new TrendsService.
func newTrendsService(sling *sling.Sling) *TrendsService {
return &TrendsService{
sling: sling.Path("trends/"),
}
}
// PlaceType represents a twitter trends PlaceType.
type PlaceType struct {
Code int `json:"code"`
Name string `json:"name"`
}
// Location reporesents a twitter Location.
type Location struct {
Country string `json:"country"`
CountryCode string `json:"countryCode"`
Name string `json:"name"`
ParentID int `json:"parentid"`
PlaceType PlaceType `json:"placeType"`
URL string `json:"url"`
WOEID int64 `json:"woeid"`
}
// Available returns the locations that Twitter has trending topic information for.
// https://dev.twitter.com/rest/reference/get/trends/available
func (s *TrendsService) Available() ([]Location, *http.Response, error) {
locations := new([]Location)
apiError := new(APIError)
resp, err := s.sling.New().Get("available.json").Receive(locations, apiError)
return *locations, resp, relevantError(err, *apiError)
}
// Trend represents a twitter trend.
type Trend struct {
Name string `json:"name"`
URL string `json:"url"`
PromotedContent string `json:"promoted_content"`
Query string `json:"query"`
TweetVolume int64 `json:"tweet_volume"`
}
// TrendsList represents a list of twitter trends.
type TrendsList struct {
Trends []Trend `json:"trends"`
AsOf string `json:"as_of"`
CreatedAt string `json:"created_at"`
Locations []TrendsLocation `json:"locations"`
}
// TrendsLocation represents a twitter trend location.
type TrendsLocation struct {
Name string `json:"name"`
WOEID int64 `json:"woeid"`
}
// TrendsPlaceParams are the parameters for Trends.Place.
type TrendsPlaceParams struct {
WOEID int64 `url:"id,omitempty"`
Exclude string `url:"exclude,omitempty"`
}
// Place returns the top 50 trending topics for a specific WOEID.
// https://dev.twitter.com/rest/reference/get/trends/place
func (s *TrendsService) Place(woeid int64, params *TrendsPlaceParams) ([]TrendsList, *http.Response, error) {
if params == nil {
params = &TrendsPlaceParams{}
}
trendsList := new([]TrendsList)
params.WOEID = woeid
apiError := new(APIError)
resp, err := s.sling.New().Get("place.json").QueryStruct(params).Receive(trendsList, apiError)
return *trendsList, resp, relevantError(err, *apiError)
}
// ClosestParams are the parameters for Trends.Closest.
type ClosestParams struct {
Lat float64 `url:"lat"`
Long float64 `url:"long"`
}
// Closest returns the locations that Twitter has trending topic information for, closest to a specified location.
// https://dev.twitter.com/rest/reference/get/trends/closest
func (s *TrendsService) Closest(params *ClosestParams) ([]Location, *http.Response, error) {
locations := new([]Location)
apiError := new(APIError)
resp, err := s.sling.New().Get("closest.json").QueryStruct(params).Receive(locations, apiError)
return *locations, resp, relevantError(err, *apiError)
}

View File

@@ -1,87 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTrendsService_Available(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/trends/available.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"country": "Sweden","countryCode": "SE","name": "Sweden","parentid": 1,"placeType": {"code": 12,"name": "Country"},"url": "http://where.yahooapis.com/v1/place/23424954","woeid": 23424954}]`)
})
expected := []Location{
Location{
Country: "Sweden",
CountryCode: "SE",
Name: "Sweden",
ParentID: 1,
PlaceType: PlaceType{Code: 12, Name: "Country"},
URL: "http://where.yahooapis.com/v1/place/23424954",
WOEID: 23424954,
},
}
client := NewClient(httpClient)
locations, _, err := client.Trends.Available()
assert.Nil(t, err)
assert.Equal(t, expected, locations)
}
func TestTrendsService_Place(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/trends/place.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"id": "123456"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"trends":[{"name":"#gotwitter"}], "as_of": "2017-02-08T16:18:18Z", "created_at": "2017-02-08T16:10:33Z","locations":[{"name": "Worldwide","woeid": 1}]}]`)
})
expected := []TrendsList{TrendsList{
Trends: []Trend{Trend{Name: "#gotwitter"}},
AsOf: "2017-02-08T16:18:18Z",
CreatedAt: "2017-02-08T16:10:33Z",
Locations: []TrendsLocation{TrendsLocation{Name: "Worldwide", WOEID: 1}},
}}
client := NewClient(httpClient)
places, _, err := client.Trends.Place(123456, &TrendsPlaceParams{})
assert.Nil(t, err)
assert.Equal(t, expected, places)
}
func TestTrendsService_Closest(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/trends/closest.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"lat": "37.781157", "long": "-122.400612831116"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"country": "Sweden","countryCode": "SE","name": "Sweden","parentid": 1,"placeType": {"code": 12,"name": "Country"},"url": "http://where.yahooapis.com/v1/place/23424954","woeid": 23424954}]`)
})
expected := []Location{
Location{
Country: "Sweden",
CountryCode: "SE",
Name: "Sweden",
ParentID: 1,
PlaceType: PlaceType{Code: 12, Name: "Country"},
URL: "http://where.yahooapis.com/v1/place/23424954",
WOEID: 23424954,
},
}
client := NewClient(httpClient)
locations, _, err := client.Trends.Closest(&ClosestParams{Lat: 37.781157, Long: -122.400612831116})
assert.Nil(t, err)
assert.Equal(t, expected, locations)
}

View File

@@ -1,61 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
const twitterAPI = "https://api.twitter.com/1.1/"
// Client is a Twitter client for making Twitter API requests.
type Client struct {
sling *sling.Sling
// Twitter API Services
Accounts *AccountService
DirectMessages *DirectMessageService
Favorites *FavoriteService
Followers *FollowerService
Friends *FriendService
Friendships *FriendshipService
Search *SearchService
Statuses *StatusService
Streams *StreamService
Timelines *TimelineService
Trends *TrendsService
Users *UserService
}
// NewClient returns a new Client.
func NewClient(httpClient *http.Client) *Client {
base := sling.New().Client(httpClient).Base(twitterAPI)
return &Client{
sling: base,
Accounts: newAccountService(base.New()),
DirectMessages: newDirectMessageService(base.New()),
Favorites: newFavoriteService(base.New()),
Followers: newFollowerService(base.New()),
Friends: newFriendService(base.New()),
Friendships: newFriendshipService(base.New()),
Search: newSearchService(base.New()),
Statuses: newStatusService(base.New()),
Streams: newStreamService(httpClient, base.New()),
Timelines: newTimelineService(base.New()),
Trends: newTrendsService(base.New()),
Users: newUserService(base.New()),
}
}
// Bool returns a new pointer to the given bool value.
func Bool(v bool) *bool {
ptr := new(bool)
*ptr = v
return ptr
}
// Float returns a new pointer to the given float64 value.
func Float(v float64) *float64 {
ptr := new(float64)
*ptr = v
return ptr
}

View File

@@ -1,93 +0,0 @@
package twitter
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var defaultTestTimeout = time.Second * 1
// testServer returns an http Client, ServeMux, and Server. The client proxies
// requests to the server and handlers can be registered on the mux to handle
// requests. The caller must close the test server.
func testServer() (*http.Client, *http.ServeMux, *httptest.Server) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
transport := &RewriteTransport{&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL)
},
}}
client := &http.Client{Transport: transport}
return client, mux, server
}
// RewriteTransport rewrites https requests to http to avoid TLS cert issues
// during testing.
type RewriteTransport struct {
Transport http.RoundTripper
}
// RoundTrip rewrites the request scheme to http and calls through to the
// composed RoundTripper or if it is nil, to the http.DefaultTransport.
func (t *RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.URL.Scheme = "http"
if t.Transport == nil {
return http.DefaultTransport.RoundTrip(req)
}
return t.Transport.RoundTrip(req)
}
func assertMethod(t *testing.T, expectedMethod string, req *http.Request) {
assert.Equal(t, expectedMethod, req.Method)
}
// assertQuery tests that the Request has the expected url query key/val pairs
func assertQuery(t *testing.T, expected map[string]string, req *http.Request) {
queryValues := req.URL.Query()
expectedValues := url.Values{}
for key, value := range expected {
expectedValues.Add(key, value)
}
assert.Equal(t, expectedValues, queryValues)
}
// assertPostForm tests that the Request has the expected key values pairs url
// encoded in its Body
func assertPostForm(t *testing.T, expected map[string]string, req *http.Request) {
req.ParseForm() // parses request Body to put url.Values in r.Form/r.PostForm
expectedValues := url.Values{}
for key, value := range expected {
expectedValues.Add(key, value)
}
assert.Equal(t, expectedValues, req.Form)
}
// assertDone asserts that the empty struct channel is closed before the given
// timeout elapses.
func assertDone(t *testing.T, ch <-chan struct{}, timeout time.Duration) {
select {
case <-ch:
_, more := <-ch
assert.False(t, more)
case <-time.After(timeout):
t.Errorf("expected channel to be closed within timeout %v", timeout)
}
}
// assertClosed asserts that the channel is closed before the given timeout
// elapses.
func assertClosed(t *testing.T, ch <-chan interface{}, timeout time.Duration) {
select {
case <-ch:
_, more := <-ch
assert.False(t, more)
case <-time.After(timeout):
t.Errorf("expected channel to be closed within timeout %v", timeout)
}
}

View File

@@ -1,122 +0,0 @@
package twitter
import (
"net/http"
"github.com/dghubble/sling"
)
// User represents a Twitter User.
// https://dev.twitter.com/overview/api/users
type User struct {
ContributorsEnabled bool `json:"contributors_enabled"`
CreatedAt string `json:"created_at"`
DefaultProfile bool `json:"default_profile"`
DefaultProfileImage bool `json:"default_profile_image"`
Description string `json:"description"`
Email string `json:"email"`
Entities *UserEntities `json:"entities"`
FavouritesCount int `json:"favourites_count"`
FollowRequestSent bool `json:"follow_request_sent"`
Following bool `json:"following"`
FollowersCount int `json:"followers_count"`
FriendsCount int `json:"friends_count"`
GeoEnabled bool `json:"geo_enabled"`
ID int64 `json:"id"`
IDStr string `json:"id_str"`
IsTranslator bool `json:"is_translator"`
Lang string `json:"lang"`
ListedCount int `json:"listed_count"`
Location string `json:"location"`
Name string `json:"name"`
Notifications bool `json:"notifications"`
ProfileBackgroundColor string `json:"profile_background_color"`
ProfileBackgroundImageURL string `json:"profile_background_image_url"`
ProfileBackgroundImageURLHttps string `json:"profile_background_image_url_https"`
ProfileBackgroundTile bool `json:"profile_background_tile"`
ProfileBannerURL string `json:"profile_banner_url"`
ProfileImageURL string `json:"profile_image_url"`
ProfileImageURLHttps string `json:"profile_image_url_https"`
ProfileLinkColor string `json:"profile_link_color"`
ProfileSidebarBorderColor string `json:"profile_sidebar_border_color"`
ProfileSidebarFillColor string `json:"profile_sidebar_fill_color"`
ProfileTextColor string `json:"profile_text_color"`
ProfileUseBackgroundImage bool `json:"profile_use_background_image"`
Protected bool `json:"protected"`
ScreenName string `json:"screen_name"`
ShowAllInlineMedia bool `json:"show_all_inline_media"`
Status *Tweet `json:"status"`
StatusesCount int `json:"statuses_count"`
Timezone string `json:"time_zone"`
URL string `json:"url"`
UtcOffset int `json:"utc_offset"`
Verified bool `json:"verified"`
WithheldInCountries []string `json:"withheld_in_countries"`
WithholdScope string `json:"withheld_scope"`
}
// UserService provides methods for accessing Twitter user API endpoints.
type UserService struct {
sling *sling.Sling
}
// newUserService returns a new UserService.
func newUserService(sling *sling.Sling) *UserService {
return &UserService{
sling: sling.Path("users/"),
}
}
// UserShowParams are the parameters for UserService.Show.
type UserShowParams struct {
UserID int64 `url:"user_id,omitempty"`
ScreenName string `url:"screen_name,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"` // whether 'status' should include entities
}
// Show returns the requested User.
// https://dev.twitter.com/rest/reference/get/users/show
func (s *UserService) Show(params *UserShowParams) (*User, *http.Response, error) {
user := new(User)
apiError := new(APIError)
resp, err := s.sling.New().Get("show.json").QueryStruct(params).Receive(user, apiError)
return user, resp, relevantError(err, *apiError)
}
// UserLookupParams are the parameters for UserService.Lookup.
type UserLookupParams struct {
UserID []int64 `url:"user_id,omitempty,comma"`
ScreenName []string `url:"screen_name,omitempty,comma"`
IncludeEntities *bool `url:"include_entities,omitempty"` // whether 'status' should include entities
}
// Lookup returns the requested Users as a slice.
// https://dev.twitter.com/rest/reference/get/users/lookup
func (s *UserService) Lookup(params *UserLookupParams) ([]User, *http.Response, error) {
users := new([]User)
apiError := new(APIError)
resp, err := s.sling.New().Get("lookup.json").QueryStruct(params).Receive(users, apiError)
return *users, resp, relevantError(err, *apiError)
}
// UserSearchParams are the parameters for UserService.Search.
type UserSearchParams struct {
Query string `url:"q,omitempty"`
Page int `url:"page,omitempty"` // 1-based page number
Count int `url:"count,omitempty"`
IncludeEntities *bool `url:"include_entities,omitempty"` // whether 'status' should include entities
}
// Search queries public user accounts.
// Requires a user auth context.
// https://dev.twitter.com/rest/reference/get/users/search
func (s *UserService) Search(query string, params *UserSearchParams) ([]User, *http.Response, error) {
if params == nil {
params = &UserSearchParams{}
}
params.Query = query
users := new([]User)
apiError := new(APIError)
resp, err := s.sling.New().Get("search.json").QueryStruct(params).Receive(users, apiError)
return *users, resp, relevantError(err, *apiError)
}

View File

@@ -1,92 +0,0 @@
package twitter
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserService_Show(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/show.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "xkcdComic"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"name": "XKCD Comic", "favourites_count": 2}`)
})
client := NewClient(httpClient)
user, _, err := client.Users.Show(&UserShowParams{ScreenName: "xkcdComic"})
expected := &User{Name: "XKCD Comic", FavouritesCount: 2}
assert.Nil(t, err)
assert.Equal(t, expected, user)
}
func TestUserService_LookupWithIds(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"user_id": "113419064,623265148"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"screen_name": "golang"}, {"screen_name": "dghubble"}]`)
})
client := NewClient(httpClient)
users, _, err := client.Users.Lookup(&UserLookupParams{UserID: []int64{113419064, 623265148}})
expected := []User{User{ScreenName: "golang"}, User{ScreenName: "dghubble"}}
assert.Nil(t, err)
assert.Equal(t, expected, users)
}
func TestUserService_LookupWithScreenNames(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/lookup.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"screen_name": "foo,bar"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"name": "Foo"}, {"name": "Bar"}]`)
})
client := NewClient(httpClient)
users, _, err := client.Users.Lookup(&UserLookupParams{ScreenName: []string{"foo", "bar"}})
expected := []User{User{Name: "Foo"}, User{Name: "Bar"}}
assert.Nil(t, err)
assert.Equal(t, expected, users)
}
func TestUserService_Search(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/search.json", func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, "GET", r)
assertQuery(t, map[string]string{"count": "11", "q": "news"}, r)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"name": "BBC"}, {"name": "BBC Breaking News"}]`)
})
client := NewClient(httpClient)
users, _, err := client.Users.Search("news", &UserSearchParams{Query: "override me", Count: 11})
expected := []User{User{Name: "BBC"}, User{Name: "BBC Breaking News"}}
assert.Nil(t, err)
assert.Equal(t, expected, users)
}
func TestUserService_SearchHandlesNilParams(t *testing.T) {
httpClient, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/1.1/users/search.json", func(w http.ResponseWriter, r *http.Request) {
assertQuery(t, map[string]string{"q": "news"}, r)
})
client := NewClient(httpClient)
client.Users.Search("news", nil)
}

View File

@@ -1,10 +0,0 @@
language: go
go:
- 1.7
- 1.8
- tip
install:
- go get github.com/golang/lint/golint
- go get -v -t .
script:
- ./test

View File

@@ -1,30 +0,0 @@
# OAuth1 Changelog
## v0.4.0 (2016-04-20)
* Add a Signer field to the Config to allow custom Signer implementations.
* Use the HMACSigner by default. This provides the same signing behavior as in previous versions (HMAC-SHA1).
* Add an RSASigner for "RSA-SHA1" OAuth1 Providers.
* Add missing Authorization Header quotes around OAuth parameter values. Many providers allowed these quotes to be missing.
* Change `Signer` to be a signer interface.
* Remove the old Signer methods `SetAccessTokenAuthHeader`, `SetRequestAuthHeader`, and `SetRequestTokenAuthHeader`.
## v0.3.0 (2015-09-13)
* Added `NoContext` which may be used in most cases.
* Allowed Transport Base http.RoundTripper to be set through a ctx.
* Changed `NewClient` to require a context.Context.
* Changed `Config.Client` to require a context.Context.
## v.0.2.0 (2015-08-30)
* Improved OAuth 1 spec compliance and test coverage.
* Added `func StaticTokenSource(*Token) TokenSource`
* Added `ParseAuthorizationCallback` function. Removed `Config.HandleAuthorizationCallback` method.
* Changed `Config` method signatures to allow an interface to be defined for the OAuth1 authorization flow. Gives users of this package (and downstream packages) the freedom to use other implementations if they wish.
* Removed `RequestToken` in favor of passing token and secret value strings.
* Removed `ReuseTokenSource` struct, it was effectively a static source. Replaced by `StaticTokenSource`.
## v0.1.0 (2015-04-26)
* Initial OAuth1 support for obtaining authorization and making authorized requests.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Dalton Hubble
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,125 +0,0 @@
# OAuth1 [![Build Status](https://travis-ci.org/dghubble/oauth1.png)](https://travis-ci.org/dghubble/oauth1) [![GoDoc](http://godoc.org/github.com/dghubble/oauth1?status.png)](http://godoc.org/github.com/dghubble/oauth1)
<img align="right" src="https://storage.googleapis.com/dghubble/oauth1.png">
OAauth1 is a Go implementation of the [OAuth 1 spec](https://tools.ietf.org/html/rfc5849).
It allows end-users to authorize a client (consumer) to access protected resources on his/her behalf and to make signed and authorized requests.
Package `oauth1` takes design cues from [golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2), to provide an analogous API and an `http.Client` with a Transport which signs/authorizes requests.
## Install
go get github.com/dghubble/oauth1
## Docs
Read [GoDoc](https://godoc.org/github.com/dghubble/oauth1)
## Usage
Package `oauth1` implements the OAuth1 authorization flow and provides an `http.Client` which can sign and authorize OAuth1 requests.
To implement "Login with X", use the [gologin](https://github.com/dghubble/gologin) packages which provide login handlers for OAuth1 and OAuth2 providers.
To call the Twitter, Digits, or Tumblr OAuth1 APIs, use the higher level Go API clients.
* [Twitter](https://github.com/dghubble/go-twitter)
* [Digits](https://github.com/dghubble/go-digits)
* [Tumblr](https://github.com/benfb/go-tumblr)
### Authorization Flow
Perform the OAuth 1 authorization flow to ask a user to grant an application access to his/her resources via an access token.
```go
import (
"github.com/dghubble/oauth1"
"github.com/dghubble/oauth1/twitter""
)
...
config := oauth1.Config{
ConsumerKey: "consumerKey",
ConsumerSecret: "consumerSecret",
CallbackURL: "http://mysite.com/oauth/twitter/callback",
Endpoint: twitter.AuthorizeEndpoint,
}
```
1. When a user performs an action (e.g. "Login with X" button calls "/login" route) get an OAuth1 request token (temporary credentials).
```go
requestToken, requestSecret, err = config.RequestToken()
// handle err
```
2. Obtain authorization from the user by redirecting them to the OAuth1 provider's authorization URL to grant the application access.
```go
authorizationURL, err := config.AuthorizationURL(requestToken)
// handle err
http.Redirect(w, req, authorizationURL.String(), http.StatusFound)
```
Receive the callback from the OAuth1 provider in a handler.
```go
requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req)
// handle err
```
3. Acquire the access token (token credentials) which can later be used to make requests on behalf of the user.
```go
accessToken, accessSecret, err := config.AccessToken(requestToken, requestSecret, verifier)
// handle error
token := oauth1.NewToken(accessToken, accessSecret)
```
Check the [examples](examples) to see this authorization flow in action from the command line, with Twitter PIN-based login and Tumblr login.
### Authorized Requests
Use an access `Token` to make authorized requests on behalf of a user.
```go
import (
"github.com/dghubble/oauth1"
)
func main() {
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("token", "tokenSecret")
// httpClient will automatically authorize http.Request's
httpClient := config.Client(oauth1.NoContext, token)
// example Twitter API request
path := "https://api.twitter.com/1.1/statuses/home_timeline.json?count=2"
resp, _ := httpClient.Get(path)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("Raw Response Body:\n%v\n", string(body))
}
```
Check the [examples](examples) to see Twitter and Tumblr requests in action.
### Concepts
An `Endpoint` groups an OAuth provider's token and authorization URL endpoints.Endpoints for common providers are provided in subpackages.
A `Config` stores a consumer application's consumer key and secret, the registered callback URL, and the `Endpoint` to which the consumer is registered. It provides OAuth1 authorization flow methods.
An OAuth1 `Token` is an access token which can be used to make signed requests on behalf of a user. See [Authorized Requests](#Authorized Requests) for details.
If you've used the [golang.org/x/oauth2](https://godoc.org/golang.org/x/oauth2) package for OAuth2 before, this organization should be familiar.
## Contributing
See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7).
## License
[MIT License](LICENSE)

View File

@@ -1,265 +0,0 @@
package oauth1
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
const (
authorizationHeaderParam = "Authorization"
authorizationPrefix = "OAuth " // trailing space is intentional
oauthConsumerKeyParam = "oauth_consumer_key"
oauthNonceParam = "oauth_nonce"
oauthSignatureParam = "oauth_signature"
oauthSignatureMethodParam = "oauth_signature_method"
oauthTimestampParam = "oauth_timestamp"
oauthTokenParam = "oauth_token"
oauthVersionParam = "oauth_version"
oauthCallbackParam = "oauth_callback"
oauthVerifierParam = "oauth_verifier"
defaultOauthVersion = "1.0"
contentType = "Content-Type"
formContentType = "application/x-www-form-urlencoded"
)
// clock provides a interface for current time providers. A Clock can be used
// in place of calling time.Now() directly.
type clock interface {
Now() time.Time
}
// A noncer provides random nonce strings.
type noncer interface {
Nonce() string
}
// auther adds an "OAuth" Authorization header field to requests.
type auther struct {
config *Config
clock clock
noncer noncer
}
func newAuther(config *Config) *auther {
return &auther{
config: config,
}
}
// setRequestTokenAuthHeader adds the OAuth1 header for the request token
// request (temporary credential) according to RFC 5849 2.1.
func (a *auther) setRequestTokenAuthHeader(req *http.Request) error {
oauthParams := a.commonOAuthParams()
oauthParams[oauthCallbackParam] = a.config.CallbackURL
params, err := collectParameters(req, oauthParams)
if err != nil {
return err
}
signatureBase := signatureBase(req, params)
signature, err := a.signer().Sign("", signatureBase)
if err != nil {
return err
}
oauthParams[oauthSignatureParam] = signature
req.Header.Set(authorizationHeaderParam, authHeaderValue(oauthParams))
return nil
}
// setAccessTokenAuthHeader sets the OAuth1 header for the access token request
// (token credential) according to RFC 5849 2.3.
func (a *auther) setAccessTokenAuthHeader(req *http.Request, requestToken, requestSecret, verifier string) error {
oauthParams := a.commonOAuthParams()
oauthParams[oauthTokenParam] = requestToken
oauthParams[oauthVerifierParam] = verifier
params, err := collectParameters(req, oauthParams)
if err != nil {
return err
}
signatureBase := signatureBase(req, params)
signature, err := a.signer().Sign(requestSecret, signatureBase)
if err != nil {
return err
}
oauthParams[oauthSignatureParam] = signature
req.Header.Set(authorizationHeaderParam, authHeaderValue(oauthParams))
return nil
}
// setRequestAuthHeader sets the OAuth1 header for making authenticated
// requests with an AccessToken (token credential) according to RFC 5849 3.1.
func (a *auther) setRequestAuthHeader(req *http.Request, accessToken *Token) error {
oauthParams := a.commonOAuthParams()
oauthParams[oauthTokenParam] = accessToken.Token
params, err := collectParameters(req, oauthParams)
if err != nil {
return err
}
signatureBase := signatureBase(req, params)
signature, err := a.signer().Sign(accessToken.TokenSecret, signatureBase)
if err != nil {
return err
}
oauthParams[oauthSignatureParam] = signature
req.Header.Set(authorizationHeaderParam, authHeaderValue(oauthParams))
return nil
}
// commonOAuthParams returns a map of the common OAuth1 protocol parameters,
// excluding the oauth_signature parameter.
func (a *auther) commonOAuthParams() map[string]string {
return map[string]string{
oauthConsumerKeyParam: a.config.ConsumerKey,
oauthSignatureMethodParam: a.signer().Name(),
oauthTimestampParam: strconv.FormatInt(a.epoch(), 10),
oauthNonceParam: a.nonce(),
oauthVersionParam: defaultOauthVersion,
}
}
// Returns a base64 encoded random 32 byte string.
func (a *auther) nonce() string {
if a.noncer != nil {
return a.noncer.Nonce()
}
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
// Returns the Unix epoch seconds.
func (a *auther) epoch() int64 {
if a.clock != nil {
return a.clock.Now().Unix()
}
return time.Now().Unix()
}
// Returns the Config's Signer or the default Signer.
func (a *auther) signer() Signer {
if a.config.Signer != nil {
return a.config.Signer
}
return &HMACSigner{ConsumerSecret: a.config.ConsumerSecret}
}
// authHeaderValue formats OAuth parameters according to RFC 5849 3.5.1. OAuth
// params are percent encoded, sorted by key (for testability), and joined by
// "=" into pairs. Pairs are joined with a ", " comma separator into a header
// string.
// The given OAuth params should include the "oauth_signature" key.
func authHeaderValue(oauthParams map[string]string) string {
pairs := sortParameters(encodeParameters(oauthParams), `%s="%s"`)
return authorizationPrefix + strings.Join(pairs, ", ")
}
// encodeParameters percent encodes parameter keys and values according to
// RFC5849 3.6 and RFC3986 2.1 and returns a new map.
func encodeParameters(params map[string]string) map[string]string {
encoded := map[string]string{}
for key, value := range params {
encoded[PercentEncode(key)] = PercentEncode(value)
}
return encoded
}
// sortParameters sorts parameters by key and returns a slice of key/value
// pairs formatted with the given format string (e.g. "%s=%s").
func sortParameters(params map[string]string, format string) []string {
// sort by key
keys := make([]string, len(params))
i := 0
for key := range params {
keys[i] = key
i++
}
sort.Strings(keys)
// parameter join
pairs := make([]string, len(params))
for i, key := range keys {
pairs[i] = fmt.Sprintf(format, key, params[key])
}
return pairs
}
// collectParameters collects request parameters from the request query, OAuth
// parameters (which should exclude oauth_signature), and the request body
// provided the body is single part, form encoded, and the form content type
// header is set. The returned map of collected parameter keys and values
// follow RFC 5849 3.4.1.3, except duplicate parameters are not supported.
func collectParameters(req *http.Request, oauthParams map[string]string) (map[string]string, error) {
// add oauth, query, and body parameters into params
params := map[string]string{}
for key, value := range req.URL.Query() {
// most backends do not accept duplicate query keys
params[key] = value[0]
}
if req.Body != nil && req.Header.Get(contentType) == formContentType {
// reads data to a []byte, draining req.Body
b, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
values, err := url.ParseQuery(string(b))
if err != nil {
return nil, err
}
for key, value := range values {
// not supporting params with duplicate keys
params[key] = value[0]
}
// reinitialize Body with ReadCloser over the []byte
req.Body = ioutil.NopCloser(bytes.NewReader(b))
}
for key, value := range oauthParams {
params[key] = value
}
return params, nil
}
// signatureBase combines the uppercase request method, percent encoded base
// string URI, and normalizes the request parameters int a parameter string.
// Returns the OAuth1 signature base string according to RFC5849 3.4.1.
func signatureBase(req *http.Request, params map[string]string) string {
method := strings.ToUpper(req.Method)
baseURL := baseURI(req)
parameterString := normalizedParameterString(params)
// signature base string constructed accoding to 3.4.1.1
baseParts := []string{method, PercentEncode(baseURL), PercentEncode(parameterString)}
return strings.Join(baseParts, "&")
}
// baseURI returns the base string URI of a request according to RFC 5849
// 3.4.1.2. The scheme and host are lowercased, the port is dropped if it
// is 80 or 443, and the path minus query parameters is included.
func baseURI(req *http.Request) string {
scheme := strings.ToLower(req.URL.Scheme)
host := strings.ToLower(req.URL.Host)
if hostPort := strings.Split(host, ":"); len(hostPort) == 2 && (hostPort[1] == "80" || hostPort[1] == "443") {
host = hostPort[0]
}
// TODO: use req.URL.EscapedPath() once Go 1.5 is more generally adopted
// For now, hacky workaround accomplishes the same internal escaping mode
// escape(u.Path, encodePath) for proper compliance with the OAuth1 spec.
path := req.URL.Path
if path != "" {
path = strings.Split(req.URL.RequestURI(), "?")[0]
}
return fmt.Sprintf("%v://%v%v", scheme, host, path)
}
// parameterString normalizes collected OAuth parameters (which should exclude
// oauth_signature) into a parameter string as defined in RFC 5894 3.4.1.3.2.
// The parameters are encoded, sorted by key, keys and values joined with "&",
// and pairs joined with "=" (e.g. foo=bar&q=gopher).
func normalizedParameterString(params map[string]string) string {
return strings.Join(sortParameters(encodeParameters(params), "%s=%s"), "&")
}

View File

@@ -1,244 +0,0 @@
package oauth1
import (
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCommonOAuthParams(t *testing.T) {
config := &Config{ConsumerKey: "some_consumer_key"}
auther := &auther{config, &fixedClock{time.Unix(50037133, 0)}, &fixedNoncer{"some_nonce"}}
expectedParams := map[string]string{
"oauth_consumer_key": "some_consumer_key",
"oauth_signature_method": "HMAC-SHA1",
"oauth_timestamp": "50037133",
"oauth_nonce": "some_nonce",
"oauth_version": "1.0",
}
assert.Equal(t, expectedParams, auther.commonOAuthParams())
}
func TestNonce(t *testing.T) {
auther := &auther{}
nonce := auther.nonce()
// assert that 32 bytes (256 bites) become 44 bytes since a base64 byte
// zeros the 2 high bits. 3 bytes convert to 4 base64 bytes, 40 base64 bytes
// represent the first 30 of 32 bytes, = padding adds another 4 byte group.
// base64 bytes = 4 * floor(bytes/3) + 4
assert.Equal(t, 44, len([]byte(nonce)))
}
func TestEpoch(t *testing.T) {
a := &auther{}
// assert that a real time is used by default
assert.InEpsilon(t, time.Now().Unix(), a.epoch(), 1)
// assert that the fixed clock can be used for testing
a = &auther{clock: &fixedClock{time.Unix(50037133, 0)}}
assert.Equal(t, int64(50037133), a.epoch())
}
func TestSigner_Default(t *testing.T) {
config := &Config{ConsumerSecret: "consumer_secret"}
a := newAuther(config)
// echo -n "hello world" | openssl dgst -sha1 -hmac "consumer_secret&token_secret" -binary | base64
expectedSignature := "BE0uILOruKfSXd4UzYlLJDfOq08="
// assert that the default signer produces the expected HMAC-SHA1 digest
method := a.signer().Name()
digest, err := a.signer().Sign("token_secret", "hello world")
assert.Nil(t, err)
assert.Equal(t, "HMAC-SHA1", method)
assert.Equal(t, expectedSignature, digest)
}
type identitySigner struct{}
func (s *identitySigner) Name() string {
return "identity"
}
func (s *identitySigner) Sign(tokenSecret, message string) (string, error) {
return message, nil
}
func TestSigner_Custom(t *testing.T) {
config := &Config{
ConsumerSecret: "consumer_secret",
Signer: &identitySigner{},
}
a := newAuther(config)
// assert that the custom signer is used
method := a.signer().Name()
digest, err := a.signer().Sign("secret", "hello world")
assert.Nil(t, err)
assert.Equal(t, "identity", method)
assert.Equal(t, "hello world", digest)
}
func TestAuthHeaderValue(t *testing.T) {
cases := []struct {
params map[string]string
authHeader string
}{
{map[string]string{}, "OAuth "},
{map[string]string{"a": "b"}, `OAuth a="b"`},
{map[string]string{"a": "b", "c": "d", "e": "f", "1": "2"}, `OAuth 1="2", a="b", c="d", e="f"`},
{map[string]string{"/= +doencode": "/= +doencode"}, `OAuth %2F%3D%20%2Bdoencode="%2F%3D%20%2Bdoencode"`},
{map[string]string{"-._~dontencode": "-._~dontencode"}, `OAuth -._~dontencode="-._~dontencode"`},
}
for _, c := range cases {
assert.Equal(t, c.authHeader, authHeaderValue(c.params))
}
}
func TestEncodeParameters(t *testing.T) {
input := map[string]string{
"a": "Dogs, Cats & Mice",
"☃": "snowman",
"ル": "ル",
}
expected := map[string]string{
"a": "Dogs%2C%20Cats%20%26%20Mice",
"%E2%98%83": "snowman",
"%E3%83%AB": "%E3%83%AB",
}
assert.Equal(t, expected, encodeParameters(input))
}
func TestSortParameters(t *testing.T) {
input := map[string]string{
".": "ape",
"5.6": "bat",
"rsa": "cat",
"%20": "dog",
"%E3%83%AB": "eel",
"dup": "fox",
//"dup": "fix", // duplicate keys not supported
}
expected := []string{
"%20=dog",
"%E3%83%AB=eel",
".=ape",
"5.6=bat",
"dup=fox",
"rsa=cat",
}
assert.Equal(t, expected, sortParameters(input, "%s=%s"))
}
func TestCollectParameters(t *testing.T) {
// example from RFC 5849 3.4.1.3.1
oauthParams := map[string]string{
"oauth_token": "kkk9d7dh3k39sjv7",
"oauth_consumer_key": "9djdj82h48djs9d2",
"oauth_signature_method": "HMAC-SHA1",
"oauth_timestamp": "137131201",
"oauth_nonce": "7d8f3e4a",
}
values := url.Values{}
values.Add("c2", "")
values.Add("plus", "2 q") // duplicate keys not supported, a3 -> plus
req, err := http.NewRequest("POST", "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b", strings.NewReader(values.Encode()))
assert.Nil(t, err)
req.Header.Set(contentType, formContentType)
params, err := collectParameters(req, oauthParams)
// assert parameters were collected from oauthParams, the query, and form body
expected := map[string]string{
"b5": "=%3D",
"a3": "a",
"c@": "",
"a2": "r b",
"oauth_token": "kkk9d7dh3k39sjv7",
"oauth_consumer_key": "9djdj82h48djs9d2",
"oauth_signature_method": "HMAC-SHA1",
"oauth_timestamp": "137131201",
"oauth_nonce": "7d8f3e4a",
"c2": "",
"plus": "2 q",
}
assert.Nil(t, err)
assert.Equal(t, expected, params)
// RFC 5849 3.4.1.3.1 requires a {"a3"="2 q"} be form encoded to "a3=2+q" in
// the application/x-www-form-urlencoded body. The parameter "2+q" should be
// read as "2 q" and percent encoded to "2%20q".
// In Go, data is form encoded by calling Encode on url.Values{} (URL
// encoding) and decoded with url.ParseQuery to url.Values. So the encoding
// of "2 q" to "2+q" and decoding back to "2 q" is handled and then params
// are percent encoded.
// http://golang.org/src/net/http/client.go#L496
// http://golang.org/src/net/http/request.go#L837
}
func TestSignatureBase(t *testing.T) {
reqA, err := http.NewRequest("get", "HTTPS://HELLO.IO?q=test", nil)
assert.Nil(t, err)
reqB, err := http.NewRequest("POST", "http://hello.io:8080", nil)
assert.Nil(t, err)
cases := []struct {
req *http.Request
params map[string]string
signatureBase string
}{
{reqA, map[string]string{"a": "b", "c": "d"}, "GET&https%3A%2F%2Fhello.io&a%3Db%26c%3Dd"},
{reqB, map[string]string{"a": "b"}, "POST&http%3A%2F%2Fhello.io%3A8080&a%3Db"},
}
// assert that method is uppercased, base uri rules applied, queries added, joined by &
for _, c := range cases {
base := signatureBase(c.req, c.params)
assert.Equal(t, c.signatureBase, base)
}
}
func TestBaseURI(t *testing.T) {
reqA, err := http.NewRequest("GET", "HTTP://EXAMPLE.COM:80/r%20v/X?id=123", nil)
assert.Nil(t, err)
reqB, err := http.NewRequest("POST", "https://www.example.net:8080/?q=1", nil)
assert.Nil(t, err)
reqC, err := http.NewRequest("POST", "https://example.com:443", nil)
cases := []struct {
req *http.Request
baseURI string
}{
{reqA, "http://example.com/r%20v/X"},
{reqB, "https://www.example.net:8080/"},
{reqC, "https://example.com"},
}
for _, c := range cases {
baseURI := baseURI(c.req)
assert.Equal(t, c.baseURI, baseURI)
}
}
func TestNormalizedParameterString(t *testing.T) {
simple := map[string]string{
"a": "b & c",
"☃": "snowman",
}
rfcExample := map[string]string{
"b5": "=%3D",
"a3": "a",
"c@": "",
"a2": "r b",
"oauth_token": "kkk9d7dh3k39sjv7",
"oauth_consumer_key": "9djdj82h48djs9d2",
"oauth_signature_method": "HMAC-SHA1",
"oauth_timestamp": "137131201",
"oauth_nonce": "7d8f3e4a",
"c2": "",
"plus": "2 q",
}
cases := []struct {
params map[string]string
parameterStr string
}{
{simple, "%E2%98%83=snowman&a=b%20%26%20c"},
{rfcExample, "a2=r%20b&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9djdj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7&plus=2%20q"},
}
for _, c := range cases {
assert.Equal(t, c.parameterStr, normalizedParameterString(c.params))
}
}

View File

@@ -1,172 +0,0 @@
package oauth1
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"golang.org/x/net/context"
)
const (
oauthTokenSecretParam = "oauth_token_secret"
oauthCallbackConfirmedParam = "oauth_callback_confirmed"
)
// Config represents an OAuth1 consumer's (client's) key and secret, the
// callback URL, and the provider Endpoint to which the consumer corresponds.
type Config struct {
// Consumer Key (Client Identifier)
ConsumerKey string
// Consumer Secret (Client Shared-Secret)
ConsumerSecret string
// Callback URL
CallbackURL string
// Provider Endpoint specifying OAuth1 endpoint URLs
Endpoint Endpoint
// OAuth1 Signer (defaults to HMAC-SHA1)
Signer Signer
}
// NewConfig returns a new Config with the given consumer key and secret.
func NewConfig(consumerKey, consumerSecret string) *Config {
return &Config{
ConsumerKey: consumerKey,
ConsumerSecret: consumerSecret,
}
}
// Client returns an HTTP client which uses the provided ctx and access Token.
func (c *Config) Client(ctx context.Context, t *Token) *http.Client {
return NewClient(ctx, c, t)
}
// NewClient returns a new http Client which signs requests via OAuth1.
func NewClient(ctx context.Context, config *Config, token *Token) *http.Client {
transport := &Transport{
Base: contextTransport(ctx),
source: StaticTokenSource(token),
auther: newAuther(config),
}
return &http.Client{Transport: transport}
}
// RequestToken obtains a Request token and secret (temporary credential) by
// POSTing a request (with oauth_callback in the auth header) to the Endpoint
// RequestTokenURL. The response body form is validated to ensure
// oauth_callback_confirmed is true. Returns the request token and secret
// (temporary credentials).
// See RFC 5849 2.1 Temporary Credentials.
func (c *Config) RequestToken() (requestToken, requestSecret string, err error) {
req, err := http.NewRequest("POST", c.Endpoint.RequestTokenURL, nil)
if err != nil {
return "", "", err
}
err = newAuther(c).setRequestTokenAuthHeader(req)
if err != nil {
return "", "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", err
}
// when err is nil, resp contains a non-nil resp.Body which must be closed
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return "", "", fmt.Errorf("oauth1: Server returned status %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
// ParseQuery to decode URL-encoded application/x-www-form-urlencoded body
values, err := url.ParseQuery(string(body))
if err != nil {
return "", "", err
}
requestToken = values.Get(oauthTokenParam)
requestSecret = values.Get(oauthTokenSecretParam)
if requestToken == "" || requestSecret == "" {
return "", "", errors.New("oauth1: Response missing oauth_token or oauth_token_secret")
}
if values.Get(oauthCallbackConfirmedParam) != "true" {
return "", "", errors.New("oauth1: oauth_callback_confirmed was not true")
}
return requestToken, requestSecret, nil
}
// AuthorizationURL accepts a request token and returns the *url.URL to the
// Endpoint's authorization page that asks the user (resource owner) for to
// authorize the consumer to act on his/her/its behalf.
// See RFC 5849 2.2 Resource Owner Authorization.
func (c *Config) AuthorizationURL(requestToken string) (*url.URL, error) {
authorizationURL, err := url.Parse(c.Endpoint.AuthorizeURL)
if err != nil {
return nil, err
}
values := authorizationURL.Query()
values.Add(oauthTokenParam, requestToken)
authorizationURL.RawQuery = values.Encode()
return authorizationURL, nil
}
// ParseAuthorizationCallback parses an OAuth1 authorization callback request
// from a provider server. The oauth_token and oauth_verifier parameters are
// parsed to return the request token from earlier in the flow and the
// verifier string.
// See RFC 5849 2.2 Resource Owner Authorization.
func ParseAuthorizationCallback(req *http.Request) (requestToken, verifier string, err error) {
// parse the raw query from the URL into req.Form
err = req.ParseForm()
if err != nil {
return "", "", err
}
requestToken = req.Form.Get(oauthTokenParam)
verifier = req.Form.Get(oauthVerifierParam)
if requestToken == "" || verifier == "" {
return "", "", errors.New("oauth1: Request missing oauth_token or oauth_verifier")
}
return requestToken, verifier, nil
}
// AccessToken obtains an access token (token credential) by POSTing a
// request (with oauth_token and oauth_verifier in the auth header) to the
// Endpoint AccessTokenURL. Returns the access token and secret (token
// credentials).
// See RFC 5849 2.3 Token Credentials.
func (c *Config) AccessToken(requestToken, requestSecret, verifier string) (accessToken, accessSecret string, err error) {
req, err := http.NewRequest("POST", c.Endpoint.AccessTokenURL, nil)
if err != nil {
return "", "", err
}
err = newAuther(c).setAccessTokenAuthHeader(req, requestToken, requestSecret, verifier)
if err != nil {
return "", "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", err
}
// when err is nil, resp contains a non-nil resp.Body which must be closed
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return "", "", fmt.Errorf("oauth1: Server returned status %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
// ParseQuery to decode URL-encoded application/x-www-form-urlencoded body
values, err := url.ParseQuery(string(body))
if err != nil {
return "", "", err
}
accessToken = values.Get(oauthTokenParam)
accessSecret = values.Get(oauthTokenSecretParam)
if accessToken == "" || accessSecret == "" {
return "", "", errors.New("oauth1: Response missing oauth_token or oauth_token_secret")
}
return accessToken, accessSecret, nil
}

View File

@@ -1,346 +0,0 @@
package oauth1
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
const expectedVerifier = "some_verifier"
func TestNewConfig(t *testing.T) {
expectedConsumerKey := "consumer_key"
expectedConsumerSecret := "consumer_secret"
config := NewConfig(expectedConsumerKey, expectedConsumerSecret)
assert.Equal(t, expectedConsumerKey, config.ConsumerKey)
assert.Equal(t, expectedConsumerSecret, config.ConsumerSecret)
}
func TestNewClient(t *testing.T) {
expectedToken := "access_token"
expectedConsumerKey := "consumer_key"
config := NewConfig(expectedConsumerKey, "consumer_secret")
token := NewToken(expectedToken, "access_secret")
client := config.Client(NoContext, token)
server := newMockServer(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "GET", req.Method)
params := parseOAuthParamsOrFail(t, req.Header.Get(authorizationHeaderParam))
assert.Equal(t, expectedToken, params[oauthTokenParam])
assert.Equal(t, expectedConsumerKey, params[oauthConsumerKeyParam])
})
defer server.Close()
client.Get(server.URL)
}
func TestNewClient_DefaultTransport(t *testing.T) {
client := NewClient(NoContext, NewConfig("t", "s"), NewToken("t", "s"))
// assert that the client uses the DefaultTransport
transport, ok := client.Transport.(*Transport)
assert.True(t, ok)
assert.Equal(t, http.DefaultTransport, transport.base())
}
func TestNewClient_ContextClientTransport(t *testing.T) {
baseTransport := &http.Transport{}
baseClient := &http.Client{Transport: baseTransport}
ctx := context.WithValue(NoContext, HTTPClient, baseClient)
client := NewClient(ctx, NewConfig("t", "s"), NewToken("t", "s"))
// assert that the client uses the ctx client's Transport as its base RoundTripper
transport, ok := client.Transport.(*Transport)
assert.True(t, ok)
assert.Equal(t, baseTransport, transport.base())
}
// newRequestTokenServer returns a new httptest.Server for an OAuth1 provider
// request token endpoint.
func newRequestTokenServer(t *testing.T, data url.Values) *httptest.Server {
return newMockServer(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "POST", req.Method)
assert.NotEmpty(t, req.Header.Get("Authorization"))
w.Header().Set(contentType, formContentType)
w.Write([]byte(data.Encode()))
})
}
// newAccessTokenServer returns a new httptest.Server for an OAuth1 provider
// access token endpoint.
func newAccessTokenServer(t *testing.T, data url.Values) *httptest.Server {
return newMockServer(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "POST", req.Method)
assert.NotEmpty(t, req.Header.Get("Authorization"))
params := parseOAuthParamsOrFail(t, req.Header.Get(authorizationHeaderParam))
assert.Equal(t, expectedVerifier, params[oauthVerifierParam])
w.Header().Set(contentType, formContentType)
w.Write([]byte(data.Encode()))
})
}
// newUnparseableBodyServer returns a new httptest.Server which writes
// responses with bodies that error when parsed by url.ParseQuery.
func newUnparseableBodyServer() *httptest.Server {
return newMockServer(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set(contentType, formContentType)
// url.ParseQuery will error, https://golang.org/src/net/url/url_test.go#L1107
w.Write([]byte("%gh&%ij"))
})
}
func TestConfigRequestToken(t *testing.T) {
expectedToken := "reqest_token"
expectedSecret := "request_secret"
data := url.Values{}
data.Add("oauth_token", expectedToken)
data.Add("oauth_token_secret", expectedSecret)
data.Add("oauth_callback_confirmed", "true")
server := newRequestTokenServer(t, data)
defer server.Close()
config := &Config{
Endpoint: Endpoint{
RequestTokenURL: server.URL,
},
}
requestToken, requestSecret, err := config.RequestToken()
assert.Nil(t, err)
assert.Equal(t, expectedToken, requestToken)
assert.Equal(t, expectedSecret, requestSecret)
}
func TestConfigRequestToken_InvalidRequestTokenURL(t *testing.T) {
config := &Config{
Endpoint: Endpoint{
RequestTokenURL: "http://wrong.com/oauth/request_token",
},
}
requestToken, requestSecret, err := config.RequestToken()
assert.NotNil(t, err)
assert.Equal(t, "", requestToken)
assert.Equal(t, "", requestSecret)
}
func TestConfigRequestToken_CallbackNotConfirmed(t *testing.T) {
const expectedToken = "reqest_token"
const expectedSecret = "request_secret"
data := url.Values{}
data.Add("oauth_token", expectedToken)
data.Add("oauth_token_secret", expectedSecret)
data.Add("oauth_callback_confirmed", "false")
server := newRequestTokenServer(t, data)
defer server.Close()
config := &Config{
Endpoint: Endpoint{
RequestTokenURL: server.URL,
},
}
requestToken, requestSecret, err := config.RequestToken()
if assert.Error(t, err) {
assert.Equal(t, "oauth1: oauth_callback_confirmed was not true", err.Error())
}
assert.Equal(t, "", requestToken)
assert.Equal(t, "", requestSecret)
}
func TestConfigRequestToken_CannotParseBody(t *testing.T) {
server := newUnparseableBodyServer()
defer server.Close()
config := &Config{
Endpoint: Endpoint{
RequestTokenURL: server.URL,
},
}
requestToken, requestSecret, err := config.RequestToken()
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "invalid URL escape")
}
assert.Equal(t, "", requestToken)
assert.Equal(t, "", requestSecret)
}
func TestConfigRequestToken_MissingTokenOrSecret(t *testing.T) {
data := url.Values{}
data.Add("oauth_token", "any_token")
data.Add("oauth_callback_confirmed", "true")
server := newRequestTokenServer(t, data)
defer server.Close()
config := &Config{
Endpoint: Endpoint{
RequestTokenURL: server.URL,
},
}
requestToken, requestSecret, err := config.RequestToken()
if assert.Error(t, err) {
assert.Equal(t, "oauth1: Response missing oauth_token or oauth_token_secret", err.Error())
}
assert.Equal(t, "", requestToken)
assert.Equal(t, "", requestSecret)
}
func TestAuthorizationURL(t *testing.T) {
expectedURL := "https://api.example.com/oauth/authorize?oauth_token=a%2Frequest_token"
config := &Config{
Endpoint: Endpoint{
AuthorizeURL: "https://api.example.com/oauth/authorize",
},
}
url, err := config.AuthorizationURL("a/request_token")
assert.Nil(t, err)
if assert.NotNil(t, url) {
assert.Equal(t, expectedURL, url.String())
}
}
func TestAuthorizationURL_CannotParseAuthorizeURL(t *testing.T) {
config := &Config{
Endpoint: Endpoint{
AuthorizeURL: "%gh&%ij",
},
}
url, err := config.AuthorizationURL("any_request_token")
assert.Nil(t, url)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "parse")
assert.Contains(t, err.Error(), "invalid URL")
}
}
func TestConfigAccessToken(t *testing.T) {
expectedToken := "access_token"
expectedSecret := "access_secret"
data := url.Values{}
data.Add("oauth_token", expectedToken)
data.Add("oauth_token_secret", expectedSecret)
server := newAccessTokenServer(t, data)
defer server.Close()
config := &Config{
Endpoint: Endpoint{
AccessTokenURL: server.URL,
},
}
accessToken, accessSecret, err := config.AccessToken("request_token", "request_secret", expectedVerifier)
assert.Nil(t, err)
assert.Equal(t, expectedToken, accessToken)
assert.Equal(t, expectedSecret, accessSecret)
}
func TestConfigAccessToken_InvalidAccessTokenURL(t *testing.T) {
config := &Config{
Endpoint: Endpoint{
AccessTokenURL: "http://wrong.com/oauth/access_token",
},
}
accessToken, accessSecret, err := config.AccessToken("any_token", "any_secret", "any_verifier")
assert.NotNil(t, err)
assert.Equal(t, "", accessToken)
assert.Equal(t, "", accessSecret)
}
func TestConfigAccessToken_CannotParseBody(t *testing.T) {
server := newUnparseableBodyServer()
defer server.Close()
config := &Config{
Endpoint: Endpoint{
AccessTokenURL: server.URL,
},
}
accessToken, accessSecret, err := config.AccessToken("any_token", "any_secret", "any_verifier")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "invalid URL escape")
}
assert.Equal(t, "", accessToken)
assert.Equal(t, "", accessSecret)
}
func TestConfigAccessToken_MissingTokenOrSecret(t *testing.T) {
data := url.Values{}
data.Add("oauth_token", "any_token")
server := newAccessTokenServer(t, data)
defer server.Close()
config := &Config{
Endpoint: Endpoint{
AccessTokenURL: server.URL,
},
}
accessToken, accessSecret, err := config.AccessToken("request_token", "request_secret", expectedVerifier)
if assert.Error(t, err) {
assert.Equal(t, "oauth1: Response missing oauth_token or oauth_token_secret", err.Error())
}
assert.Equal(t, "", accessToken)
assert.Equal(t, "", accessSecret)
}
func TestParseAuthorizationCallback_GET(t *testing.T) {
expectedToken := "token"
expectedVerifier := "verifier"
server := newMockServer(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "GET", req.Method)
// logic under test
requestToken, verifier, err := ParseAuthorizationCallback(req)
assert.Nil(t, err)
assert.Equal(t, expectedToken, requestToken)
assert.Equal(t, expectedVerifier, verifier)
})
defer server.Close()
// OAuth1 provider calls callback url
url, err := url.Parse(server.URL)
assert.Nil(t, err)
query := url.Query()
query.Add("oauth_token", expectedToken)
query.Add("oauth_verifier", expectedVerifier)
url.RawQuery = query.Encode()
http.Get(url.String())
}
func TestParseAuthorizationCallback_POST(t *testing.T) {
expectedToken := "token"
expectedVerifier := "verifier"
server := newMockServer(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "POST", req.Method)
// logic under test
requestToken, verifier, err := ParseAuthorizationCallback(req)
assert.Nil(t, err)
assert.Equal(t, expectedToken, requestToken)
assert.Equal(t, expectedVerifier, verifier)
})
defer server.Close()
// OAuth1 provider calls callback url
form := url.Values{}
form.Add("oauth_token", expectedToken)
form.Add("oauth_verifier", expectedVerifier)
http.PostForm(server.URL, form)
}
func TestParseAuthorizationCallback_MissingTokenOrVerifier(t *testing.T) {
server := newMockServer(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "GET", req.Method)
// logic under test
requestToken, verifier, err := ParseAuthorizationCallback(req)
if assert.Error(t, err) {
assert.Equal(t, "oauth1: Request missing oauth_token or oauth_verifier", err.Error())
}
assert.Equal(t, "", requestToken)
assert.Equal(t, "", verifier)
})
defer server.Close()
// OAuth1 provider calls callback url
url, err := url.Parse(server.URL)
assert.Nil(t, err)
query := url.Query()
query.Add("oauth_token", "any_token")
query.Add("oauth_verifier", "") // missing oauth_verifier
url.RawQuery = query.Encode()
http.Get(url.String())
}

View File

@@ -1,24 +0,0 @@
package oauth1
import (
"net/http"
"golang.org/x/net/context"
)
type contextKey struct{}
// HTTPClient is the context key to associate an *http.Client value with
// a context.
var HTTPClient contextKey
// NoContext is the default context to use in most cases.
var NoContext = context.TODO()
// contextTransport gets the Transport from the context client or nil.
func contextTransport(ctx context.Context) http.RoundTripper {
if client, ok := ctx.Value(HTTPClient).(*http.Client); ok {
return client.Transport
}
return nil
}

View File

@@ -1,21 +0,0 @@
package oauth1
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestContextTransport(t *testing.T) {
client := &http.Client{
Transport: http.DefaultTransport,
}
ctx := context.WithValue(NoContext, HTTPClient, client)
assert.Equal(t, http.DefaultTransport, contextTransport(ctx))
}
func TestContextTransport_NoContextClient(t *testing.T) {
assert.Nil(t, contextTransport(NoContext))
}

View File

@@ -1,97 +0,0 @@
/*
Package oauth1 is a Go implementation of the OAuth1 spec RFC 5849.
It allows end-users to authorize a client (consumer) to access protected
resources on their behalf (e.g. login) and allows clients to make signed and
authorized requests on behalf of a user (e.g. API calls).
It takes design cues from golang.org/x/oauth2, providing an http.Client which
handles request signing and authorization.
Usage
Package oauth1 implements the OAuth1 authorization flow and provides an
http.Client which can sign and authorize OAuth1 requests.
To implement "Login with X", use the https://github.com/dghubble/gologin
packages which provide login handlers for OAuth1 and OAuth2 providers.
To call the Twitter, Digits, or Tumblr OAuth1 APIs, use the higher level Go API
clients.
* https://github.com/dghubble/go-twitter
* https://github.com/dghubble/go-digits
* https://github.com/benfb/go-tumblr
Authorization Flow
Perform the OAuth 1 authorization flow to ask a user to grant an application
access to his/her resources via an access token.
import (
"github.com/dghubble/oauth1"
"github.com/dghubble/oauth1/twitter""
)
...
config := oauth1.Config{
ConsumerKey: "consumerKey",
ConsumerSecret: "consumerSecret",
CallbackURL: "http://mysite.com/oauth/twitter/callback",
Endpoint: twitter.AuthorizeEndpoint,
}
1. When a user performs an action (e.g. "Login with X" button calls "/login"
route) get an OAuth1 request token (temporary credentials).
requestToken, requestSecret, err = config.RequestToken()
// handle err
2. Obtain authorization from the user by redirecting them to the OAuth1
provider's authorization URL to grant the application access.
authorizationURL, err := config.AuthorizationURL(requestToken)
// handle err
http.Redirect(w, req, authorizationURL.String(), htt.StatusFound)
Receive the callback from the OAuth1 provider in a handler.
requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req)
// handle err
3. Acquire the access token (token credentials) which can later be used
to make requests on behalf of the user.
accessToken, accessSecret, err := config.AccessToken(requestToken, requestSecret, verifier)
// handle error
token := oauth1.NewToken(accessToken, accessSecret)
Check the examples to see this authorization flow in action from the command
line, with Twitter PIN-based login and Tumblr login.
Authorized Requests
Use an access Token to make authorized requests on behalf of a user.
import (
"github.com/dghubble/oauth1"
)
func main() {
config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("token", "tokenSecret")
// httpClient will automatically authorize http.Request's
httpClient := config.Client(token)
// example Twitter API request
path := "https://api.twitter.com/1.1/statuses/home_timeline.json?count=2"
resp, _ := httpClient.Get(path)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("Raw Response Body:\n%v\n", string(body))
}
Check the examples to see Twitter and Tumblr requests in action.
*/
package oauth1

View File

@@ -1,13 +0,0 @@
// Package dropbox provides constants for using OAuth1 to access Dropbox.
package dropbox
import (
"github.com/dghubble/oauth1"
)
// Endpoint is Dropbox's OAuth 1 endpoint.
var Endpoint = oauth1.Endpoint{
RequestTokenURL: "https://api.dropbox.com/1/oauth/request_token",
AuthorizeURL: "https://api.dropbox.com/1/oauth/authorize",
AccessTokenURL: "https://api.dropbox.com/1/oauth/access_token",
}

View File

@@ -1,36 +0,0 @@
package oauth1
import (
"bytes"
"fmt"
)
// PercentEncode percent encodes a string according to RFC 3986 2.1.
func PercentEncode(input string) string {
var buf bytes.Buffer
for _, b := range []byte(input) {
// if in unreserved set
if shouldEscape(b) {
buf.Write([]byte(fmt.Sprintf("%%%02X", b)))
} else {
// do not escape, write byte as-is
buf.WriteByte(b)
}
}
return buf.String()
}
// shouldEscape returns false if the byte is an unreserved character that
// should not be escaped and true otherwise, according to RFC 3986 2.1.
func shouldEscape(c byte) bool {
// RFC3986 2.3 unreserved characters
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
return false
}
switch c {
case '-', '.', '_', '~':
return false
}
// all other bytes must be escaped
return true
}

View File

@@ -1,27 +0,0 @@
package oauth1
import (
"testing"
)
func TestPercentEncode(t *testing.T) {
cases := []struct {
input string
expected string
}{
{" ", "%20"},
{"%", "%25"},
{"&", "%26"},
{"-._", "-._"},
{" /=+", "%20%2F%3D%2B"},
{"Ladies + Gentlemen", "Ladies%20%2B%20Gentlemen"},
{"An encoded string!", "An%20encoded%20string%21"},
{"Dogs, Cats & Mice", "Dogs%2C%20Cats%20%26%20Mice"},
{"☃", "%E2%98%83"},
}
for _, c := range cases {
if output := PercentEncode(c.input); output != c.expected {
t.Errorf("expected %s, got %s", c.expected, output)
}
}
}

View File

@@ -1,12 +0,0 @@
package oauth1
// Endpoint represents an OAuth1 provider's (server's) request token,
// owner authorization, and access token request URLs.
type Endpoint struct {
// Request URL (Temporary Credential Request URI)
RequestTokenURL string
// Authorize URL (Resource Owner Authorization URI)
AuthorizeURL string
// Access Token URL (Token Request URI)
AccessTokenURL string
}

Some files were not shown because too many files have changed in this diff Show More