1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-18 12:49:42 +02:00

Bump version to 5.11.0

This commit is contained in:
Harvey Kandola 2024-01-10 14:47:40 -05:00
parent a32510b8e6
commit 510e1bd0bd
370 changed files with 18825 additions and 5454 deletions

View file

@ -4,7 +4,7 @@ COPY ./gui /go/src/github.com/documize/community/gui
RUN npm --network-timeout=100000 install
RUN npm run build -- --environment=production --output-path dist-prod --suppress-sizes true
FROM golang:1.19-alpine as builder
FROM golang:1.21-alpine as builder
WORKDIR /go/src/github.com/documize/community
COPY . /go/src/github.com/documize/community
COPY --from=frontbuilder /go/src/github.com/documize/community/gui/dist-prod/assets /go/src/github.com/documize/community/edition/static/public/assets

View file

@ -40,9 +40,9 @@ func main() {
// Specify the product edition.
rt.Product = domain.Product{}
rt.Product.Major = "5"
rt.Product.Minor = "10"
rt.Product.Minor = "11"
rt.Product.Patch = "0"
rt.Product.Revision = "1695054225"
rt.Product.Revision = "1704915883"
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = domain.CommunityEdition
rt.Product.Title = "Community"

View file

@ -1,6 +1,6 @@
{
"name": "documize",
"version": "5.10.0",
"version": "5.11.0",
"private": true,
"description": "Documize Community",
"repository": "",

View file

@ -6,21 +6,5 @@
"theme_color": "#280A42",
"description": "Organized documents",
"start_url": "/?utm_source=homescreen",
"icons": [{
"src": "assets/img/icon.png",
"sizes": "64x64",
"type": "image/png"
}, {
"src": "assets/img/icon.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "assets/img/icon.png",
"sizes": "256x256",
"type": "image/png"
}, {
"src": "assets/img/icon.png",
"sizes": "1024x1024",
"type": "image/png"
}]
"icons": []
}

41
vendor/github.com/Azure/go-ntlmssp/SECURITY.md generated vendored Normal file
View file

@ -0,0 +1,41 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

View file

@ -42,7 +42,7 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) {
}
target, user := toUnicode(m.TargetName), toUnicode(m.UserName)
workstation := toUnicode("go-ntlmssp")
workstation := toUnicode("")
ptr := binary.Size(&authenticateMessageFields{})
f := authenticateMessageFields{
@ -82,7 +82,7 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) {
//ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message
//that was received from the server
func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byte, error) {
func ProcessChallenge(challengeMessageData []byte, user, password string, domainNeeded bool) ([]byte, error) {
if user == "" && password == "" {
return nil, errors.New("Anonymous authentication not supported")
}
@ -98,6 +98,10 @@ func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byt
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) {
return nil, errors.New("Key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)")
}
if !domainNeeded {
cm.TargetName = ""
}
am := authenicateMessage{
UserName: user,

View file

@ -5,26 +5,55 @@ import (
"strings"
)
type authheader string
type authheader []string
func (h authheader) IsBasic() bool {
return strings.HasPrefix(string(h), "Basic ")
for _, s := range h {
if strings.HasPrefix(string(s), "Basic ") {
return true
}
}
return false
}
func (h authheader) Basic() string {
for _, s := range h {
if strings.HasPrefix(string(s), "Basic ") {
return s
}
}
return ""
}
func (h authheader) IsNegotiate() bool {
return strings.HasPrefix(string(h), "Negotiate")
for _, s := range h {
if strings.HasPrefix(string(s), "Negotiate") {
return true
}
}
return false
}
func (h authheader) IsNTLM() bool {
return strings.HasPrefix(string(h), "NTLM")
for _, s := range h {
if strings.HasPrefix(string(s), "NTLM") {
return true
}
}
return false
}
func (h authheader) GetData() ([]byte, error) {
p := strings.Split(string(h), " ")
if len(p) < 2 {
return nil, nil
for _, s := range h {
if strings.HasPrefix(string(s), "NTLM") || strings.HasPrefix(string(s), "Negotiate") || strings.HasPrefix(string(s), "Basic ") {
p := strings.Split(string(s), " ")
if len(p) < 2 {
return nil, nil
}
return base64.StdEncoding.DecodeString(string(p[1]))
}
}
return base64.StdEncoding.DecodeString(string(p[1]))
return nil, nil
}
func (h authheader) GetBasicCreds() (username, password string, err error) {

View file

@ -10,15 +10,22 @@ import (
)
// GetDomain : parse domain name from based on slashes in the input
func GetDomain(user string) (string, string) {
// Need to check for upn as well
func GetDomain(user string) (string, string, bool) {
domain := ""
domainNeeded := false
if strings.Contains(user, "\\") {
ucomponents := strings.SplitN(user, "\\", 2)
domain = ucomponents[0]
user = ucomponents[1]
domainNeeded = true
} else if strings.Contains(user, "@") {
domainNeeded = false
} else {
domainNeeded = true
}
return user, domain
return user, domain, domainNeeded
}
//Negotiator is a http.Roundtripper decorator that automatically
@ -34,10 +41,11 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
rt = http.DefaultTransport
}
// If it is not basic auth, just round trip the request as usual
reqauth := authheader(req.Header.Get("Authorization"))
reqauth := authheader(req.Header.Values("Authorization"))
if !reqauth.IsBasic() {
return rt.RoundTrip(req)
}
reqauthBasic := reqauth.Basic()
// Save request body
body := bytes.Buffer{}
if req.Body != nil {
@ -59,11 +67,10 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if res.StatusCode != http.StatusUnauthorized {
return res, err
}
resauth := authheader(res.Header.Get("Www-Authenticate"))
resauth := authheader(res.Header.Values("Www-Authenticate"))
if !resauth.IsNegotiate() && !resauth.IsNTLM() {
// Unauthorized, Negotiate not requested, let's try with basic auth
req.Header.Set("Authorization", string(reqauth))
req.Header.Set("Authorization", string(reqauthBasic))
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
@ -75,7 +82,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
if res.StatusCode != http.StatusUnauthorized {
return res, err
}
resauth = authheader(res.Header.Get("Www-Authenticate"))
resauth = authheader(res.Header.Values("Www-Authenticate"))
}
if resauth.IsNegotiate() || resauth.IsNTLM() {
@ -91,7 +98,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
// get domain from username
domain := ""
u, domain = GetDomain(u)
u, domain, domainNeeded := GetDomain(u)
// send negotiate
negotiateMessage, err := NewNegotiateMessage(domain, "")
@ -112,7 +119,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
}
// receive challenge?
resauth = authheader(res.Header.Get("Www-Authenticate"))
resauth = authheader(res.Header.Values("Www-Authenticate"))
challengeMessage, err := resauth.GetData()
if err != nil {
return nil, err
@ -125,7 +132,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error)
res.Body.Close()
// send authenticate
authenticateMessage, err := ProcessChallenge(challengeMessage, u, p)
authenticateMessage, err := ProcessChallenge(challengeMessage, u, p, domainNeeded)
if err != nil {
return nil, err
}

View file

@ -1,5 +1,2 @@
TAGS
tags
.*.swp
tomlcheck/tomlcheck
toml.test
/toml.test
/toml-test

View file

@ -1,15 +0,0 @@
language: go
go:
- 1.1
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- tip
install:
- go install ./...
- go get github.com/BurntSushi/toml-test
script:
- export PATH="$PATH:$HOME/gopath/bin"
- make test

View file

@ -1,3 +0,0 @@
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/v0.4.0/versions/en/toml-v0.4.0.md)

View file

@ -1,19 +0,0 @@
install:
go install ./...
test: install
go test -v
toml-test toml-test-decoder
toml-test -encoder toml-test-encoder
fmt:
gofmt -w *.go */*.go
colcheck *.go */*.go
tags:
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS
push:
git push origin master
git push github master

View file

@ -1,46 +1,26 @@
## TOML parser and encoder for Go with reflection
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml`
packages. This package also supports the `encoding.TextUnmarshaler` and
`encoding.TextMarshaler` interfaces so that you can define custom data
representations. (There is an example of this below.)
reflection interface similar to Go's standard library `json` and `xml` packages.
Spec: https://github.com/toml-lang/toml
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
Compatible with TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
Documentation: https://godocs.io/github.com/BurntSushi/toml
Documentation: https://godoc.org/github.com/BurntSushi/toml
See the [releases page](https://github.com/BurntSushi/toml/releases) for a
changelog; this information is also in the git tag annotations (e.g. `git show
v0.4.0`).
Installation:
This library requires Go 1.13 or newer; add it to your go.mod with:
```bash
go get github.com/BurntSushi/toml
```
% go get github.com/BurntSushi/toml@latest
Try the toml validator:
It also comes with a TOML validator CLI tool:
```bash
go get github.com/BurntSushi/toml/cmd/tomlv
tomlv some-toml-file.toml
```
[![Build Status](https://travis-ci.org/BurntSushi/toml.svg?branch=master)](https://travis-ci.org/BurntSushi/toml) [![GoDoc](https://godoc.org/github.com/BurntSushi/toml?status.svg)](https://godoc.org/github.com/BurntSushi/toml)
### Testing
This package passes all tests in
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder
and the encoder.
% go install github.com/BurntSushi/toml/cmd/tomlv@latest
% tomlv some-toml-file.toml
### Examples
This package works similarly to how the Go standard library handles `XML`
and `JSON`. Namely, data is loaded into Go values via reflection.
For the simplest example, consider some TOML file as just a list of keys
and values:
For the simplest example, consider some TOML file as just a list of keys and
values:
```toml
Age = 25
@ -50,29 +30,23 @@ Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
```
Which could be defined in Go as:
Which can be decoded with:
```go
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time // requires `import time`
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time
}
```
And then decoded with:
```go
var conf Config
if _, err := toml.Decode(tomlData, &conf); err != nil {
// handle error
}
_, err := toml.Decode(tomlData, &conf)
```
You can also use struct tags if your struct field name doesn't map to a TOML
key value directly:
You can also use struct tags if your struct field name doesn't map to a TOML key
value directly:
```toml
some_key_NAME = "wat"
@ -80,139 +54,67 @@ some_key_NAME = "wat"
```go
type TOML struct {
ObscureKey string `toml:"some_key_NAME"`
ObscureKey string `toml:"some_key_NAME"`
}
```
### Using the `encoding.TextUnmarshaler` interface
Beware that like other decoders **only exported fields** are considered when
encoding and decoding; private fields are silently ignored.
Here's an example that automatically parses duration strings into
`time.Duration` values:
### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces
Here's an example that automatically parses values in a `mail.Address`:
```toml
[[song]]
name = "Thunder Road"
duration = "4m49s"
[[song]]
name = "Stairway to Heaven"
duration = "8m03s"
```
Which can be decoded with:
```go
type song struct {
Name string
Duration duration
}
type songs struct {
Song []song
}
var favorites songs
if _, err := toml.Decode(blob, &favorites); err != nil {
log.Fatal(err)
}
for _, s := range favorites.Song {
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
```
And you'll also need a `duration` type that satisfies the
`encoding.TextUnmarshaler` interface:
```go
type duration struct {
time.Duration
}
func (d *duration) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
return err
}
```
### More complex usage
Here's an example of how to load the example from the official spec page:
```toml
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
contacts = [
"Donald Duck <donald@duckburg.com>",
"Scrooge McDuck <scrooge@duckburg.com>",
]
```
And the corresponding Go types are:
Can be decoded with:
```go
type tomlConfig struct {
Title string
Owner ownerInfo
DB database `toml:"database"`
Servers map[string]server
Clients clients
// Create address type which satisfies the encoding.TextUnmarshaler interface.
type address struct {
*mail.Address
}
type ownerInfo struct {
Name string
Org string `toml:"organization"`
Bio string
DOB time.Time
func (a *address) UnmarshalText(text []byte) error {
var err error
a.Address, err = mail.ParseAddress(string(text))
return err
}
type database struct {
Server string
Ports []int
ConnMax int `toml:"connection_max"`
Enabled bool
}
// Decode it.
func decode() {
blob := `
contacts = [
"Donald Duck <donald@duckburg.com>",
"Scrooge McDuck <scrooge@duckburg.com>",
]
`
type server struct {
IP string
DC string
}
var contacts struct {
Contacts []address
}
type clients struct {
Data [][]interface{}
Hosts []string
_, err := toml.Decode(blob, &contacts)
if err != nil {
log.Fatal(err)
}
for _, c := range contacts.Contacts {
fmt.Printf("%#v\n", c.Address)
}
// Output:
// &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
// &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
}
```
Note that a case insensitive match will be tried if an exact match can't be
found.
To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
a similar way.
A working example of the above can be found in `_examples/example.{go,toml}`.
### More complex usage
See the [`_example/`](/_example) directory for a more complex example.

View file

@ -1,157 +1,199 @@
package toml
import (
"bytes"
"encoding"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math"
"os"
"reflect"
"strconv"
"strings"
"time"
)
func e(format string, args ...interface{}) error {
return fmt.Errorf("toml: "+format, args...)
}
// Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves.
type Unmarshaler interface {
UnmarshalTOML(interface{}) error
}
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
func Unmarshal(p []byte, v interface{}) error {
_, err := Decode(string(p), v)
// Unmarshal decodes the contents of data in TOML format into a pointer v.
//
// See [Decoder] for a description of the decoding process.
func Unmarshal(data []byte, v interface{}) error {
_, err := NewDecoder(bytes.NewReader(data)).Decode(v)
return err
}
// Decode the TOML data in to the pointer v.
//
// See [Decoder] for a description of the decoding process.
func Decode(data string, v interface{}) (MetaData, error) {
return NewDecoder(strings.NewReader(data)).Decode(v)
}
// DecodeFile reads the contents of a file and decodes it with [Decode].
func DecodeFile(path string, v interface{}) (MetaData, error) {
fp, err := os.Open(path)
if err != nil {
return MetaData{}, err
}
defer fp.Close()
return NewDecoder(fp).Decode(v)
}
// Primitive is a TOML value that hasn't been decoded into a Go value.
// When using the various `Decode*` functions, the type `Primitive` may
// be given to any value, and its decoding will be delayed.
//
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
// This type can be used for any value, which will cause decoding to be delayed.
// You can use [PrimitiveDecode] to "manually" decode these values.
//
// The underlying representation of a `Primitive` value is subject to change.
// Do not rely on it.
// NOTE: The underlying representation of a `Primitive` value is subject to
// change. Do not rely on it.
//
// N.B. Primitive values are still parsed, so using them will only avoid
// the overhead of reflection. They can be useful when you don't know the
// exact type of TOML data until run time.
// NOTE: Primitive values are still parsed, so using them will only avoid the
// overhead of reflection. They can be useful when you don't know the exact type
// of TOML data until runtime.
type Primitive struct {
undecoded interface{}
context Key
}
// DEPRECATED!
// The significand precision for float32 and float64 is 24 and 53 bits; this is
// the range a natural number can be stored in a float without loss of data.
const (
maxSafeFloat32Int = 16777215 // 2^24-1
maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
)
// Decoder decodes TOML data.
//
// Use MetaData.PrimitiveDecode instead.
func PrimitiveDecode(primValue Primitive, v interface{}) error {
md := MetaData{decoded: make(map[string]bool)}
return md.unify(primValue.undecoded, rvalue(v))
// TOML tables correspond to Go structs or maps; they can be used
// interchangeably, but structs offer better type safety.
//
// TOML table arrays correspond to either a slice of structs or a slice of maps.
//
// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the
// local timezone.
//
// [time.Duration] types are treated as nanoseconds if the TOML value is an
// integer, or they're parsed with time.ParseDuration() if they're strings.
//
// All other TOML types (float, string, int, bool and array) correspond to the
// obvious Go types.
//
// An exception to the above rules is if a type implements the TextUnmarshaler
// interface, in which case any primitive TOML value (floats, strings, integers,
// booleans, datetimes) will be converted to a []byte and given to the value's
// UnmarshalText method. See the Unmarshaler example for a demonstration with
// email addresses.
//
// # Key mapping
//
// TOML keys can map to either keys in a Go map or field names in a Go struct.
// The special `toml` struct tag can be used to map TOML keys to struct fields
// that don't match the key name exactly (see the example). A case insensitive
// match to struct names will be tried if an exact match can't be found.
//
// The mapping between TOML values and Go values is loose. That is, there may
// exist TOML values that cannot be placed into your representation, and there
// may be parts of your representation that do not correspond to TOML values.
// This loose mapping can be made stricter by using the IsDefined and/or
// Undecoded methods on the MetaData returned.
//
// This decoder does not handle cyclic types. Decode will not terminate if a
// cyclic type is passed.
type Decoder struct {
r io.Reader
}
// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
// including this method. (i.e., `v` may contain more `Primitive`
// values.)
// NewDecoder creates a new Decoder.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
var (
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
)
// Decode TOML data in to the pointer `v`.
func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
s := "%q"
if reflect.TypeOf(v) == nil {
s = "%v"
}
return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
}
// Check if this is a supported type: struct, map, interface{}, or something
// that implements UnmarshalTOML or UnmarshalText.
rv = indirect(rv)
rt := rv.Type()
if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
!(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
!rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
}
// TODO: parser should read from io.Reader? Or at the very least, make it
// read from []byte rather than string
data, err := ioutil.ReadAll(dec.r)
if err != nil {
return MetaData{}, err
}
p, err := parse(string(data))
if err != nil {
return MetaData{}, err
}
md := MetaData{
mapping: p.mapping,
keyInfo: p.keyInfo,
keys: p.ordered,
decoded: make(map[string]struct{}, len(p.ordered)),
context: nil,
data: data,
}
return md, md.unify(p.mapping, rv)
}
// PrimitiveDecode is just like the other Decode* functions, except it decodes a
// TOML value that has already been parsed. Valid primitive values can *only* be
// obtained from values filled by the decoder functions, including this method.
// (i.e., v may contain more [Primitive] values.)
//
// Meta data for primitive values is included in the meta data returned by
// the `Decode*` functions with one exception: keys returned by the Undecoded
// method will only reflect keys that were decoded. Namely, any keys hidden
// behind a Primitive will be considered undecoded. Executing this method will
// update the undecoded keys in the meta data. (See the example.)
// Meta data for primitive values is included in the meta data returned by the
// Decode* functions with one exception: keys returned by the Undecoded method
// will only reflect keys that were decoded. Namely, any keys hidden behind a
// Primitive will be considered undecoded. Executing this method will update the
// undecoded keys in the meta data. (See the example.)
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
md.context = primValue.context
defer func() { md.context = nil }()
return md.unify(primValue.undecoded, rvalue(v))
}
// Decode will decode the contents of `data` in TOML format into a pointer
// `v`.
//
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
// used interchangeably.)
//
// TOML arrays of tables correspond to either a slice of structs or a slice
// of maps.
//
// TOML datetimes correspond to Go `time.Time` values.
//
// All other TOML types (float, string, int, bool and array) correspond
// to the obvious Go types.
//
// An exception to the above rules is if a type implements the
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
// (floats, strings, integers, booleans and datetimes) will be converted to
// a byte string and given to the value's UnmarshalText method. See the
// Unmarshaler example for a demonstration with time duration strings.
//
// Key mapping
//
// TOML keys can map to either keys in a Go map or field names in a Go
// struct. The special `toml` struct tag may be used to map TOML keys to
// struct fields that don't match the key name exactly. (See the example.)
// A case insensitive match to struct names will be tried if an exact match
// can't be found.
//
// The mapping between TOML values and Go values is loose. That is, there
// may exist TOML values that cannot be placed into your representation, and
// there may be parts of your representation that do not correspond to
// TOML values. This loose mapping can be made stricter by using the IsDefined
// and/or Undecoded methods on the MetaData returned.
//
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
}
if rv.IsNil() {
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
}
p, err := parse(data)
if err != nil {
return MetaData{}, err
}
md := MetaData{
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, indirect(rv))
}
// DecodeFile is just like Decode, except it will automatically read the
// contents of the file at `fpath` and decode it for you.
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadFile(fpath)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// DecodeReader is just like Decode, except it will consume all bytes
// from the reader and decode it for you.
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadAll(r)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// unify performs a sort of type unification based on the structure of `rv`,
// which is the client representation.
//
// Any type mismatch produces an error. Finding a type that we don't know
// how to handle produces an unsupported type error.
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
// Special case. Look for a `Primitive` value.
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
// TODO: #76 would make this superfluous after implemented.
if rv.Type() == primitiveType {
// Save the undecoded data and the key context into the primitive
// value.
context := make(Key, len(md.context))
@ -163,36 +205,24 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
return nil
}
// Special case. Unmarshaler Interface support.
if rv.CanAddr() {
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
return v.UnmarshalTOML(data)
}
rvi := rv.Interface()
if v, ok := rvi.(Unmarshaler); ok {
return v.UnmarshalTOML(data)
}
// Special case. Handle time.Time values specifically.
// TODO: Remove this code when we decide to drop support for Go 1.1.
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding
// interfaces.
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) {
return md.unifyDatetime(data, rv)
}
// Special case. Look for a value satisfying the TextUnmarshaler interface.
if v, ok := rv.Interface().(TextUnmarshaler); ok {
if v, ok := rvi.(encoding.TextUnmarshaler); ok {
return md.unifyText(data, v)
}
// BUG(burntsushi)
// TODO:
// The behavior here is incorrect whenever a Go type satisfies the
// encoding.TextUnmarshaler interface but also corresponds to a TOML
// hash or array. In particular, the unmarshaler should only be applied
// to primitive TOML values. But at this point, it will be applied to
// all kinds of values and produce an incorrect error whenever those values
// are hashes or arrays (including arrays of tables).
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
// array. In particular, the unmarshaler should only be applied to primitive
// TOML values. But at this point, it will be applied to all kinds of values
// and produce an incorrect error whenever those values are hashes or arrays
// (including arrays of tables).
k := rv.Kind()
// laziness
if k >= reflect.Int && k <= reflect.Uint64 {
return md.unifyInt(data, rv)
}
@ -218,17 +248,14 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
case reflect.Bool:
return md.unifyBool(data, rv)
case reflect.Interface:
// we only support empty interfaces.
if rv.NumMethod() > 0 {
return e("unsupported type %s", rv.Type())
if rv.NumMethod() > 0 { /// Only empty interfaces are supported.
return md.e("unsupported type %s", rv.Type())
}
return md.unifyAnything(data, rv)
case reflect.Float32:
fallthrough
case reflect.Float64:
case reflect.Float32, reflect.Float64:
return md.unifyFloat64(data, rv)
}
return e("unsupported type %s", rv.Kind())
return md.e("unsupported type %s", rv.Kind())
}
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
@ -237,7 +264,7 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
if mapping == nil {
return nil
}
return e("type mismatch for %s: expected table but found %T",
return md.e("type mismatch for %s: expected table but found %T",
rv.Type().String(), mapping)
}
@ -259,17 +286,18 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
for _, i := range f.index {
subv = indirect(subv.Field(i))
}
if isUnifiable(subv) {
md.decoded[md.context.add(key).String()] = true
md.decoded[md.context.add(key).String()] = struct{}{}
md.context = append(md.context, key)
if err := md.unify(datum, subv); err != nil {
err := md.unify(datum, subv)
if err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" {
// Bad user! No soup for you!
return e("cannot write unexported field %s.%s",
rv.Type().String(), f.name)
return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
}
}
}
@ -277,28 +305,43 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
}
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
keyType := rv.Type().Key().Kind()
if keyType != reflect.String && keyType != reflect.Interface {
return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
keyType, rv.Type())
}
tmap, ok := mapping.(map[string]interface{})
if !ok {
if tmap == nil {
return nil
}
return badtype("map", mapping)
return md.badtype("map", mapping)
}
if rv.IsNil() {
rv.Set(reflect.MakeMap(rv.Type()))
}
for k, v := range tmap {
md.decoded[md.context.add(k).String()] = true
md.decoded[md.context.add(k).String()] = struct{}{}
md.context = append(md.context, k)
rvkey := indirect(reflect.New(rv.Type().Key()))
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
if err := md.unify(v, rvval); err != nil {
err := md.unify(v, indirect(rvval))
if err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
rvkey.SetString(k)
rvkey := indirect(reflect.New(rv.Type().Key()))
switch keyType {
case reflect.Interface:
rvkey.Set(reflect.ValueOf(k))
case reflect.String:
rvkey.SetString(k)
}
rv.SetMapIndex(rvkey, rvval)
}
return nil
@ -310,12 +353,10 @@ func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
return md.badtype("slice", data)
}
sliceLen := datav.Len()
if sliceLen != rv.Len() {
return e("expected array length %d; got TOML array of length %d",
rv.Len(), sliceLen)
if l := datav.Len(); l != rv.Len() {
return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
}
return md.unifySliceArray(datav, rv)
}
@ -326,7 +367,7 @@ func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
if !datav.IsValid() {
return nil
}
return badtype("slice", data)
return md.badtype("slice", data)
}
n := datav.Len()
if rv.IsNil() || rv.Cap() < n {
@ -337,37 +378,45 @@ func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
}
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
sliceLen := data.Len()
for i := 0; i < sliceLen; i++ {
v := data.Index(i).Interface()
sliceval := indirect(rv.Index(i))
if err := md.unify(v, sliceval); err != nil {
l := data.Len()
for i := 0; i < l; i++ {
err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i)))
if err != nil {
return err
}
}
return nil
}
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
if _, ok := data.(time.Time); ok {
rv.Set(reflect.ValueOf(data))
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
_, ok := rv.Interface().(json.Number)
if ok {
if i, ok := data.(int64); ok {
rv.SetString(strconv.FormatInt(i, 10))
} else if f, ok := data.(float64); ok {
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
} else {
return md.badtype("string", data)
}
return nil
}
return badtype("time.Time", data)
}
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
if s, ok := data.(string); ok {
rv.SetString(s)
return nil
}
return badtype("string", data)
return md.badtype("string", data)
}
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
rvk := rv.Kind()
if num, ok := data.(float64); ok {
switch rv.Kind() {
switch rvk {
case reflect.Float32:
if num < -math.MaxFloat32 || num > math.MaxFloat32 {
return md.parseErr(errParseRange{i: num, size: rvk.String()})
}
fallthrough
case reflect.Float64:
rv.SetFloat(num)
@ -376,54 +425,60 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
}
return nil
}
return badtype("float", data)
if num, ok := data.(int64); ok {
if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
(rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
return md.parseErr(errParseRange{i: num, size: rvk.String()})
}
rv.SetFloat(float64(num))
return nil
}
return md.badtype("float", data)
}
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
if num, ok := data.(int64); ok {
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
switch rv.Kind() {
case reflect.Int, reflect.Int64:
// No bounds checking necessary.
case reflect.Int8:
if num < math.MinInt8 || num > math.MaxInt8 {
return e("value %d is out of range for int8", num)
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("value %d is out of range for int16", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("value %d is out of range for int32", num)
}
_, ok := rv.Interface().(time.Duration)
if ok {
// Parse as string duration, and fall back to regular integer parsing
// (as nanosecond) if this is not a string.
if s, ok := data.(string); ok {
dur, err := time.ParseDuration(s)
if err != nil {
return md.parseErr(errParseDuration{s})
}
rv.SetInt(num)
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
unum := uint64(num)
switch rv.Kind() {
case reflect.Uint, reflect.Uint64:
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("value %d is out of range for uint8", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("value %d is out of range for uint16", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("value %d is out of range for uint32", num)
}
}
rv.SetUint(unum)
} else {
panic("unreachable")
rv.SetInt(int64(dur))
return nil
}
return nil
}
return badtype("integer", data)
num, ok := data.(int64)
if !ok {
return md.badtype("integer", data)
}
rvk := rv.Kind()
switch {
case rvk >= reflect.Int && rvk <= reflect.Int64:
if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
(rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
(rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
return md.parseErr(errParseRange{i: num, size: rvk.String()})
}
rv.SetInt(num)
case rvk >= reflect.Uint && rvk <= reflect.Uint64:
unum := uint64(num)
if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
return md.parseErr(errParseRange{i: num, size: rvk.String()})
}
rv.SetUint(unum)
default:
panic("unreachable")
}
return nil
}
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
@ -431,7 +486,7 @@ func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
rv.SetBool(b)
return nil
}
return badtype("boolean", data)
return md.badtype("boolean", data)
}
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
@ -439,10 +494,16 @@ func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
return nil
}
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error {
var s string
switch sdata := data.(type) {
case TextMarshaler:
case Marshaler:
text, err := sdata.MarshalTOML()
if err != nil {
return err
}
s = string(text)
case encoding.TextMarshaler:
text, err := sdata.MarshalText()
if err != nil {
return err
@ -459,7 +520,7 @@ func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
case float64:
s = fmt.Sprintf("%f", sdata)
default:
return badtype("primitive (string-like)", data)
return md.badtype("primitive (string-like)", data)
}
if err := v.UnmarshalText([]byte(s)); err != nil {
return err
@ -467,22 +528,54 @@ func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
return nil
}
func (md *MetaData) badtype(dst string, data interface{}) error {
return md.e("incompatible types: TOML value has type %T; destination has type %s", data, dst)
}
func (md *MetaData) parseErr(err error) error {
k := md.context.String()
return ParseError{
LastKey: k,
Position: md.keyInfo[k].pos,
Line: md.keyInfo[k].pos.Line,
err: err,
input: string(md.data),
}
}
func (md *MetaData) e(format string, args ...interface{}) error {
f := "toml: "
if len(md.context) > 0 {
f = fmt.Sprintf("toml: (last key %q): ", md.context)
p := md.keyInfo[md.context.String()].pos
if p.Line > 0 {
f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
}
}
return fmt.Errorf(f+format, args...)
}
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
func rvalue(v interface{}) reflect.Value {
return indirect(reflect.ValueOf(v))
}
// indirect returns the value pointed to by a pointer.
// Pointers are followed until the value is not a pointer.
// New values are allocated for each nil pointer.
//
// An exception to this rule is if the value satisfies an interface of
// interest to us (like encoding.TextUnmarshaler).
// Pointers are followed until the value is not a pointer. New values are
// allocated for each nil pointer.
//
// An exception to this rule is if the value satisfies an interface of interest
// to us (like encoding.TextUnmarshaler).
func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr {
if v.CanSet() {
pv := v.Addr()
if _, ok := pv.Interface().(TextUnmarshaler); ok {
pvi := pv.Interface()
if _, ok := pvi.(encoding.TextUnmarshaler); ok {
return pv
}
if _, ok := pvi.(Unmarshaler); ok {
return pv
}
}
@ -498,12 +591,12 @@ func isUnifiable(rv reflect.Value) bool {
if rv.CanSet() {
return true
}
if _, ok := rv.Interface().(TextUnmarshaler); ok {
rvi := rv.Interface()
if _, ok := rvi.(encoding.TextUnmarshaler); ok {
return true
}
if _, ok := rvi.(Unmarshaler); ok {
return true
}
return false
}
func badtype(expected string, data interface{}) error {
return e("cannot load TOML value of type %T into a Go %s", data, expected)
}

19
vendor/github.com/BurntSushi/toml/decode_go116.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
//go:build go1.16
// +build go1.16
package toml
import (
"io/fs"
)
// DecodeFS reads the contents of a file from [fs.FS] and decodes it with
// [Decode].
func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) {
fp, err := fsys.Open(path)
if err != nil {
return MetaData{}, err
}
defer fp.Close()
return NewDecoder(fp).Decode(v)
}

29
vendor/github.com/BurntSushi/toml/deprecated.go generated vendored Normal file
View file

@ -0,0 +1,29 @@
package toml
import (
"encoding"
"io"
)
// TextMarshaler is an alias for encoding.TextMarshaler.
//
// Deprecated: use encoding.TextMarshaler
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is an alias for encoding.TextUnmarshaler.
//
// Deprecated: use encoding.TextUnmarshaler
type TextUnmarshaler encoding.TextUnmarshaler
// PrimitiveDecode is an alias for MetaData.PrimitiveDecode().
//
// Deprecated: use MetaData.PrimitiveDecode.
func PrimitiveDecode(primValue Primitive, v interface{}) error {
md := MetaData{decoded: make(map[string]struct{})}
return md.unify(primValue.undecoded, rvalue(v))
}
// DecodeReader is an alias for NewDecoder(r).Decode(v).
//
// Deprecated: use NewDecoder(reader).Decode(&value).
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) }

View file

@ -1,27 +1,11 @@
/*
Package toml provides facilities for decoding and encoding TOML configuration
files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/toml-lang/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
type of each key in a TOML document.
Testing
There are two important types of tests used for this package. The first is
contained inside '*_test.go' files and uses the standard Go unit testing
framework. These tests are primarily devoted to holistically testing the
decoder and encoder.
The second type of testing is used to verify the implementation's adherence
to the TOML specification. These tests have been factored into their own
project: https://github.com/BurntSushi/toml-test
The reason the tests are in a separate project is so that they can be used by
any implementation of TOML. Namely, it is language agnostic.
*/
// Package toml implements decoding and encoding of TOML files.
//
// This package supports TOML v1.0.0, as specified at https://toml.io
//
// There is also support for delaying decoding with the Primitive type, and
// querying the set of keys in a TOML document with the MetaData type.
//
// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
// and can be used to verify if TOML document is valid. It can also be used to
// print the type of each key.
package toml

View file

@ -2,57 +2,127 @@ package toml
import (
"bufio"
"encoding"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/BurntSushi/toml/internal"
)
type tomlEncodeError struct{ error }
var (
errArrayMixedElementTypes = errors.New(
"toml: cannot encode array with mixed element types")
errArrayNilElement = errors.New(
"toml: cannot encode array with nil element")
errNonString = errors.New(
"toml: cannot encode a map with non-string key type")
errAnonNonStruct = errors.New(
"toml: cannot encode an anonymous field that is not a struct")
errArrayNoTable = errors.New(
"toml: TOML array element cannot contain a table")
errNoKey = errors.New(
"toml: top-level values must be Go maps or structs")
errAnything = errors.New("") // used in testing
errArrayNilElement = errors.New("toml: cannot encode array with nil element")
errNonString = errors.New("toml: cannot encode a map with non-string key type")
errNoKey = errors.New("toml: top-level values must be Go maps or structs")
errAnything = errors.New("") // used in testing
)
var quotedReplacer = strings.NewReplacer(
"\t", "\\t",
"\n", "\\n",
"\r", "\\r",
var dblQuotedReplacer = strings.NewReplacer(
"\"", "\\\"",
"\\", "\\\\",
"\x00", `\u0000`,
"\x01", `\u0001`,
"\x02", `\u0002`,
"\x03", `\u0003`,
"\x04", `\u0004`,
"\x05", `\u0005`,
"\x06", `\u0006`,
"\x07", `\u0007`,
"\b", `\b`,
"\t", `\t`,
"\n", `\n`,
"\x0b", `\u000b`,
"\f", `\f`,
"\r", `\r`,
"\x0e", `\u000e`,
"\x0f", `\u000f`,
"\x10", `\u0010`,
"\x11", `\u0011`,
"\x12", `\u0012`,
"\x13", `\u0013`,
"\x14", `\u0014`,
"\x15", `\u0015`,
"\x16", `\u0016`,
"\x17", `\u0017`,
"\x18", `\u0018`,
"\x19", `\u0019`,
"\x1a", `\u001a`,
"\x1b", `\u001b`,
"\x1c", `\u001c`,
"\x1d", `\u001d`,
"\x1e", `\u001e`,
"\x1f", `\u001f`,
"\x7f", `\u007f`,
)
// Encoder controls the encoding of Go values to a TOML document to some
// io.Writer.
//
// The indentation level can be controlled with the Indent field.
type Encoder struct {
// A single indentation level. By default it is two spaces.
Indent string
var (
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
)
// hasWritten is whether we have written any output to w yet.
hasWritten bool
w *bufio.Writer
// Marshaler is the interface implemented by types that can marshal themselves
// into valid TOML.
type Marshaler interface {
MarshalTOML() ([]byte, error)
}
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
// given. By default, a single indentation level is 2 spaces.
// Encoder encodes a Go to a TOML document.
//
// The mapping between Go values and TOML values should be precisely the same as
// for [Decode].
//
// time.Time is encoded as a RFC 3339 string, and time.Duration as its string
// representation.
//
// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to
// encoding the value as custom TOML.
//
// If you want to write arbitrary binary data then you will need to use
// something like base64 since TOML does not have any binary types.
//
// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
// are encoded first.
//
// Go maps will be sorted alphabetically by key for deterministic output.
//
// The toml struct tag can be used to provide the key name; if omitted the
// struct field name will be used. If the "omitempty" option is present the
// following value will be skipped:
//
// - arrays, slices, maps, and string with len of 0
// - struct with all zero values
// - bool false
//
// If omitzero is given all int and float types with a value of 0 will be
// skipped.
//
// Encoding Go values without a corresponding TOML representation will return an
// error. Examples of this includes maps with non-string keys, slices with nil
// elements, embedded non-struct types, and nested slices containing maps or
// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
// is okay, as is []map[string][]string).
//
// NOTE: only exported keys are encoded due to the use of reflection. Unexported
// keys are silently discarded.
type Encoder struct {
// String to use for a single indentation level; default is two spaces.
Indent string
w *bufio.Writer
hasWritten bool // written any output to w yet?
}
// NewEncoder create a new Encoder.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: bufio.NewWriter(w),
@ -60,32 +130,14 @@ func NewEncoder(w io.Writer) *Encoder {
}
}
// Encode writes a TOML representation of the Go value to the underlying
// io.Writer. If the value given cannot be encoded to a valid TOML document,
// then an error is returned.
// Encode writes a TOML representation of the Go value to the [Encoder]'s writer.
//
// The mapping between Go values and TOML values should be precisely the same
// as for the Decode* functions. Similarly, the TextMarshaler interface is
// supported by encoding the resulting bytes as strings. (If you want to write
// arbitrary binary data then you will need to use something like base64 since
// TOML does not have any binary types.)
//
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
// sub-hashes are encoded first.
//
// If a Go map is encoded, then its keys are sorted alphabetically for
// deterministic output. More control over this behavior may be provided if
// there is demand for it.
//
// Encoding Go values without a corresponding TOML representation---like map
// types with non-string keys---will cause an error to be returned. Similarly
// for mixed arrays/slices, arrays/slices with nil elements, embedded
// non-struct types and nested slices containing maps or structs.
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
// and so is []map[string][]string.)
// An error is returned if the value given cannot be encoded to a valid TOML
// document.
func (enc *Encoder) Encode(v interface{}) error {
rv := eindirect(reflect.ValueOf(v))
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
err := enc.safeEncode(Key([]string{}), rv)
if err != nil {
return err
}
return enc.w.Flush()
@ -106,13 +158,15 @@ func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
}
func (enc *Encoder) encode(key Key, rv reflect.Value) {
// Special case. Time needs to be in ISO8601 format.
// Special case. If we can marshal the type to text, then we used that.
// Basically, this prevents the encoder for handling these types as
// generic structs (or whatever the underlying type of a TextMarshaler is).
switch rv.Interface().(type) {
case time.Time, TextMarshaler:
enc.keyEqElement(key, rv)
// If we can marshal the type to text, then we use that. This prevents the
// encoder for handling these types as generic structs (or whatever the
// underlying type of a TextMarshaler is).
switch {
case isMarshaler(rv):
enc.writeKeyValue(key, rv, false)
return
case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
return
}
@ -123,12 +177,12 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
enc.keyEqElement(key, rv)
enc.writeKeyValue(key, rv, false)
case reflect.Array, reflect.Slice:
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
enc.eArrayOfTables(key, rv)
} else {
enc.keyEqElement(key, rv)
enc.writeKeyValue(key, rv, false)
}
case reflect.Interface:
if rv.IsNil() {
@ -148,55 +202,114 @@ func (enc *Encoder) encode(key Key, rv reflect.Value) {
case reflect.Struct:
enc.eTable(key, rv)
default:
panic(e("unsupported type for key '%s': %s", key, k))
encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k))
}
}
// eElement encodes any value that can be an array element (primitives and
// arrays).
// eElement encodes any value that can be an array element.
func (enc *Encoder) eElement(rv reflect.Value) {
switch v := rv.Interface().(type) {
case time.Time:
// Special case time.Time as a primitive. Has to come before
// TextMarshaler below because time.Time implements
// encoding.TextMarshaler, but we need to always use UTC.
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
return
case TextMarshaler:
// Special case. Use text marshaler if it's available for this value.
if s, err := v.MarshalText(); err != nil {
encPanic(err)
} else {
enc.writeQuoted(string(s))
case time.Time: // Using TextMarshaler adds extra quotes, which we don't want.
format := time.RFC3339Nano
switch v.Location() {
case internal.LocalDatetime:
format = "2006-01-02T15:04:05.999999999"
case internal.LocalDate:
format = "2006-01-02"
case internal.LocalTime:
format = "15:04:05.999999999"
}
switch v.Location() {
default:
enc.wf(v.Format(format))
case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
enc.wf(v.In(time.UTC).Format(format))
}
return
case Marshaler:
s, err := v.MarshalTOML()
if err != nil {
encPanic(err)
}
if s == nil {
encPanic(errors.New("MarshalTOML returned nil and no error"))
}
enc.w.Write(s)
return
case encoding.TextMarshaler:
s, err := v.MarshalText()
if err != nil {
encPanic(err)
}
if s == nil {
encPanic(errors.New("MarshalText returned nil and no error"))
}
enc.writeQuoted(string(s))
return
case time.Duration:
enc.writeQuoted(v.String())
return
case json.Number:
n, _ := rv.Interface().(json.Number)
if n == "" { /// Useful zero value.
enc.w.WriteByte('0')
return
} else if v, err := n.Int64(); err == nil {
enc.eElement(reflect.ValueOf(v))
return
} else if v, err := n.Float64(); err == nil {
enc.eElement(reflect.ValueOf(v))
return
}
encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n))
}
switch rv.Kind() {
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
case reflect.Float64:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
case reflect.Interface:
case reflect.Ptr:
enc.eElement(rv.Elem())
return
case reflect.String:
enc.writeQuoted(rv.String())
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
f := rv.Float()
if math.IsNaN(f) {
enc.wf("nan")
} else if math.IsInf(f, 0) {
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
} else {
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
}
case reflect.Float64:
f := rv.Float()
if math.IsNaN(f) {
enc.wf("nan")
} else if math.IsInf(f, 0) {
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
} else {
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
}
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
case reflect.Struct:
enc.eStruct(nil, rv, true)
case reflect.Map:
enc.eMap(nil, rv, true)
case reflect.Interface:
enc.eElement(rv.Elem())
default:
panic(e("unexpected primitive type: %s", rv.Kind()))
encPanic(fmt.Errorf("unexpected type: %T", rv.Interface()))
}
}
// By the TOML spec, all floats must have a decimal with at least one
// number on either side.
// By the TOML spec, all floats must have a decimal with at least one number on
// either side.
func floatAddDecimal(fstr string) string {
if !strings.Contains(fstr, ".") {
return fstr + ".0"
@ -205,14 +318,14 @@ func floatAddDecimal(fstr string) string {
}
func (enc *Encoder) writeQuoted(s string) {
enc.wf("\"%s\"", quotedReplacer.Replace(s))
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.wf("[")
for i := 0; i < length; i++ {
elem := rv.Index(i)
elem := eindirect(rv.Index(i))
enc.eElement(elem)
if i != length-1 {
enc.wf(", ")
@ -226,44 +339,43 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
encPanic(errNoKey)
}
for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i)
trv := eindirect(rv.Index(i))
if isNil(trv) {
continue
}
panicIfInvalidKey(key)
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
enc.wf("%s[[%s]]", enc.indentStr(key), key)
enc.newline()
enc.eMapOrStruct(key, trv)
enc.eMapOrStruct(key, trv, false)
}
}
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
panicIfInvalidKey(key)
if len(key) == 1 {
// Output an extra newline between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}
if len(key) > 0 {
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
enc.wf("%s[%s]", enc.indentStr(key), key)
enc.newline()
}
enc.eMapOrStruct(key, rv)
enc.eMapOrStruct(key, rv, false)
}
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
switch rv := eindirect(rv); rv.Kind() {
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
switch rv.Kind() {
case reflect.Map:
enc.eMap(key, rv)
enc.eMap(key, rv, inline)
case reflect.Struct:
enc.eStruct(key, rv)
enc.eStruct(key, rv, inline)
default:
// Should never happen?
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
}
}
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
rt := rv.Type()
if rt.Key().Kind() != reflect.String {
encPanic(errNonString)
@ -274,68 +386,100 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value) {
var mapKeysDirect, mapKeysSub []string
for _, mapKey := range rv.MapKeys() {
k := mapKey.String()
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
mapKeysSub = append(mapKeysSub, k)
} else {
mapKeysDirect = append(mapKeysDirect, k)
}
}
var writeMapKeys = func(mapKeys []string) {
var writeMapKeys = func(mapKeys []string, trailC bool) {
sort.Strings(mapKeys)
for _, mapKey := range mapKeys {
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
if isNil(mrv) {
// Don't write anything for nil fields.
for i, mapKey := range mapKeys {
val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
if isNil(val) {
continue
}
enc.encode(key.add(mapKey), mrv)
if inline {
enc.writeKeyValue(Key{mapKey}, val, true)
if trailC || i != len(mapKeys)-1 {
enc.wf(", ")
}
} else {
enc.encode(key.add(mapKey), val)
}
}
}
writeMapKeys(mapKeysDirect)
writeMapKeys(mapKeysSub)
if inline {
enc.wf("{")
}
writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
writeMapKeys(mapKeysSub, false)
if inline {
enc.wf("}")
}
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
const is32Bit = (32 << (^uint(0) >> 63)) == 32
func pointerTo(t reflect.Type) reflect.Type {
if t.Kind() == reflect.Ptr {
return pointerTo(t.Elem())
}
return t
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
// Write keys for fields directly under this key first, because if we write
// a field that creates a new table, then all keys under it will be in that
// a field that creates a new table then all keys under it will be in that
// table (not the one we're writing here).
rt := rv.Type()
var fieldsDirect, fieldsSub [][]int
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
//
// Fields is a [][]int: for fieldsDirect this always has one entry (the
// struct index). For fieldsSub it contains two entries: the parent field
// index from tv, and the field indexes for the fields of the sub.
var (
rt = rv.Type()
fieldsDirect, fieldsSub [][]int
addFields func(rt reflect.Type, rv reflect.Value, start []int)
)
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
// skip unexported fields
if f.PkgPath != "" && !f.Anonymous {
isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
continue
}
frv := rv.Field(i)
if f.Anonymous {
t := f.Type
switch t.Kind() {
case reflect.Struct:
// Treat anonymous struct fields with
// tag names as though they are not
// anonymous, like encoding/json does.
if getOptions(f.Tag).name == "" {
addFields(t, frv, f.Index)
continue
}
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct &&
getOptions(f.Tag).name == "" {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), f.Index)
}
continue
}
// Fall through to the normal field encoding logic below
// for non-struct anonymous fields.
opts := getOptions(f.Tag)
if opts.skip {
continue
}
frv := eindirect(rv.Field(i))
if is32Bit {
// Copy so it works correct on 32bit archs; not clear why this
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
// This also works fine on 64bit, but 32bit archs are somewhat
// rare and this is a wee bit faster.
copyStart := make([]int, len(start))
copy(copyStart, start)
start = copyStart
}
// Treat anonymous struct fields with tag names as though they are
// not anonymous, like encoding/json does.
//
// Non-struct anonymous fields use the normal encoding logic.
if isEmbed {
if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
addFields(frv.Type(), frv, append(start, f.Index...))
continue
}
}
if typeIsHash(tomlTypeOfGo(frv)) {
if typeIsTable(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
@ -344,48 +488,81 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
}
addFields(rt, rv, nil)
var writeFields = func(fields [][]int) {
writeFields := func(fields [][]int) {
for _, fieldIndex := range fields {
sft := rt.FieldByIndex(fieldIndex)
sf := rv.FieldByIndex(fieldIndex)
if isNil(sf) {
// Don't write anything for nil fields.
continue
}
fieldType := rt.FieldByIndex(fieldIndex)
fieldVal := rv.FieldByIndex(fieldIndex)
opts := getOptions(sft.Tag)
opts := getOptions(fieldType.Tag)
if opts.skip {
continue
}
keyName := sft.Name
if opts.omitempty && isEmpty(fieldVal) {
continue
}
fieldVal = eindirect(fieldVal)
if isNil(fieldVal) { /// Don't write anything for nil fields.
continue
}
keyName := fieldType.Name
if opts.name != "" {
keyName = opts.name
}
if opts.omitempty && isEmpty(sf) {
continue
}
if opts.omitzero && isZero(sf) {
if opts.omitzero && isZero(fieldVal) {
continue
}
enc.encode(key.add(keyName), sf)
if inline {
enc.writeKeyValue(Key{keyName}, fieldVal, true)
if fieldIndex[0] != len(fields)-1 {
enc.wf(", ")
}
} else {
enc.encode(key.add(keyName), fieldVal)
}
}
}
if inline {
enc.wf("{")
}
writeFields(fieldsDirect)
writeFields(fieldsSub)
if inline {
enc.wf("}")
}
}
// tomlTypeName returns the TOML type name of the Go value's type. It is
// used to determine whether the types of array elements are mixed (which is
// forbidden). If the Go value is nil, then it is illegal for it to be an array
// element, and valueIsNil is returned as true.
// Returns the TOML type of a Go value. The type may be `nil`, which means
// no concrete TOML type could be found.
// tomlTypeOfGo returns the TOML type name of the Go value's type.
//
// It is used to determine whether the types of array elements are mixed (which
// is forbidden). If the Go value is nil, then it is illegal for it to be an
// array element, and valueIsNil is returned as true.
//
// The type may be `nil`, which means no concrete TOML type could be found.
func tomlTypeOfGo(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() {
return nil
}
if rv.Kind() == reflect.Struct {
if rv.Type() == timeType {
return tomlDatetime
}
if isMarshaler(rv) {
return tomlString
}
return tomlHash
}
if isMarshaler(rv) {
return tomlString
}
switch rv.Kind() {
case reflect.Bool:
return tomlBool
@ -397,7 +574,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
case reflect.Float32, reflect.Float64:
return tomlFloat
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
if isTableArray(rv) {
return tomlArrayHash
}
return tomlArray
@ -407,54 +584,35 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
return tomlString
case reflect.Map:
return tomlHash
case reflect.Struct:
switch rv.Interface().(type) {
case time.Time:
return tomlDatetime
case TextMarshaler:
return tomlString
default:
return tomlHash
}
default:
panic("unexpected reflect.Kind: " + rv.Kind().String())
encPanic(errors.New("unsupported type: " + rv.Kind().String()))
panic("unreachable")
}
}
// tomlArrayType returns the element type of a TOML array. The type returned
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
// slize). This function may also panic if it finds a type that cannot be
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
}
firstType := tomlTypeOfGo(rv.Index(0))
if firstType == nil {
encPanic(errArrayNilElement)
func isMarshaler(rv reflect.Value) bool {
return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
}
// isTableArray reports if all entries in the array or slice are a table.
func isTableArray(arr reflect.Value) bool {
if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
return false
}
rvlen := rv.Len()
for i := 1; i < rvlen; i++ {
elem := rv.Index(i)
switch elemType := tomlTypeOfGo(elem); {
case elemType == nil:
ret := true
for i := 0; i < arr.Len(); i++ {
tt := tomlTypeOfGo(eindirect(arr.Index(i)))
// Don't allow nil.
if tt == nil {
encPanic(errArrayNilElement)
case !typeEqual(firstType, elemType):
encPanic(errArrayMixedElementTypes)
}
if ret && !typeEqual(tomlHash, tt) {
ret = false
}
}
// If we have a nested array, then we must make sure that the nested
// array contains ONLY primitives.
// This checks arbitrarily nested arrays.
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
nest := tomlArrayType(eindirect(rv.Index(0)))
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
encPanic(errArrayNoTable)
}
}
return firstType
return ret
}
type tagOptions struct {
@ -499,8 +657,26 @@ func isEmpty(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0
case reflect.Struct:
if rv.Type().Comparable() {
return reflect.Zero(rv.Type()).Interface() == rv.Interface()
}
// Need to also check if all the fields are empty, otherwise something
// like this with uncomparable types will always return true:
//
// type a struct{ field b }
// type b struct{ s []string }
// s := a{field: b{s: []string{"AAA"}}}
for i := 0; i < rv.NumField(); i++ {
if !isEmpty(rv.Field(i)) {
return false
}
}
return true
case reflect.Bool:
return !rv.Bool()
case reflect.Ptr:
return rv.IsNil()
}
return false
}
@ -511,18 +687,34 @@ func (enc *Encoder) newline() {
}
}
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
// Write a key/value pair:
//
// key = <any value>
//
// This is also used for "k = v" in inline tables; so something like this will
// be written in three calls:
//
// ┌───────────────────┐
// │ ┌───┐ ┌────┐│
// v v v v vv
// key = {k = 1, k2 = 2}
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
/// Marshaler used on top-level document; call eElement() to just call
/// Marshal{TOML,Text}.
if len(key) == 0 {
encPanic(errNoKey)
enc.eElement(val)
return
}
panicIfInvalidKey(key)
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val)
enc.newline()
if !inline {
enc.newline()
}
}
func (enc *Encoder) wf(format string, v ...interface{}) {
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
_, err := fmt.Fprintf(enc.w, format, v...)
if err != nil {
encPanic(err)
}
enc.hasWritten = true
@ -536,13 +728,25 @@ func encPanic(err error) {
panic(tomlEncodeError{err})
}
// Resolve any level of pointers to the actual value (e.g. **string → string).
func eindirect(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return eindirect(v.Elem())
default:
if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
if isMarshaler(v) {
return v
}
if v.CanAddr() { /// Special case for marshalers; see #358.
if pv := v.Addr(); isMarshaler(pv) {
return pv
}
}
return v
}
if v.IsNil() {
return v
}
return eindirect(v.Elem())
}
func isNil(rv reflect.Value) bool {
@ -553,16 +757,3 @@ func isNil(rv reflect.Value) bool {
return false
}
}
func panicIfInvalidKey(key Key) {
for _, k := range key {
if len(k) == 0 {
encPanic(e("Key '%s' is not a valid table name. Key names "+
"cannot be empty.", key.maybeQuotedAll()))
}
}
}
func isValidKeyName(s string) bool {
return len(s) != 0
}

View file

@ -1,19 +0,0 @@
// +build go1.2
package toml
// In order to support Go 1.1, we define our own TextMarshaler and
// TextUnmarshaler types. For Go 1.2+, we just alias them with the
// standard library interfaces.
import (
"encoding"
)
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler encoding.TextUnmarshaler

View file

@ -1,18 +0,0 @@
// +build !go1.2
package toml
// These interfaces were introduced in Go 1.2, so we add them manually when
// compiling for Go 1.1.
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined
// here so that Go 1.1 can be supported.
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}

279
vendor/github.com/BurntSushi/toml/error.go generated vendored Normal file
View file

@ -0,0 +1,279 @@
package toml
import (
"fmt"
"strings"
)
// ParseError is returned when there is an error parsing the TOML syntax such as
// invalid syntax, duplicate keys, etc.
//
// In addition to the error message itself, you can also print detailed location
// information with context by using [ErrorWithPosition]:
//
// toml: error: Key 'fruit' was already created and cannot be used as an array.
//
// At line 4, column 2-7:
//
// 2 | fruit = []
// 3 |
// 4 | [[fruit]] # Not allowed
// ^^^^^
//
// [ErrorWithUsage] can be used to print the above with some more detailed usage
// guidance:
//
// toml: error: newlines not allowed within inline tables
//
// At line 1, column 18:
//
// 1 | x = [{ key = 42 #
// ^
//
// Error help:
//
// Inline tables must always be on a single line:
//
// table = {key = 42, second = 43}
//
// It is invalid to split them over multiple lines like so:
//
// # INVALID
// table = {
// key = 42,
// second = 43
// }
//
// Use regular for this:
//
// [table]
// key = 42
// second = 43
type ParseError struct {
Message string // Short technical message.
Usage string // Longer message with usage guidance; may be blank.
Position Position // Position of the error
LastKey string // Last parsed key, may be blank.
// Line the error occurred.
//
// Deprecated: use [Position].
Line int
err error
input string
}
// Position of an error.
type Position struct {
Line int // Line number, starting at 1.
Start int // Start of error, as byte offset starting at 0.
Len int // Lenght in bytes.
}
func (pe ParseError) Error() string {
msg := pe.Message
if msg == "" { // Error from errorf()
msg = pe.err.Error()
}
if pe.LastKey == "" {
return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg)
}
return fmt.Sprintf("toml: line %d (last key %q): %s",
pe.Position.Line, pe.LastKey, msg)
}
// ErrorWithPosition returns the error with detailed location context.
//
// See the documentation on [ParseError].
func (pe ParseError) ErrorWithPosition() string {
if pe.input == "" { // Should never happen, but just in case.
return pe.Error()
}
var (
lines = strings.Split(pe.input, "\n")
col = pe.column(lines)
b = new(strings.Builder)
)
msg := pe.Message
if msg == "" {
msg = pe.err.Error()
}
// TODO: don't show control characters as literals? This may not show up
// well everywhere.
if pe.Position.Len == 1 {
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
msg, pe.Position.Line, col+1)
} else {
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
msg, pe.Position.Line, col, col+pe.Position.Len)
}
if pe.Position.Line > 2 {
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3])
}
if pe.Position.Line > 1 {
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2])
}
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1])
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len))
return b.String()
}
// ErrorWithUsage returns the error with detailed location context and usage
// guidance.
//
// See the documentation on [ParseError].
func (pe ParseError) ErrorWithUsage() string {
m := pe.ErrorWithPosition()
if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
for i := range lines {
if lines[i] != "" {
lines[i] = " " + lines[i]
}
}
return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
}
return m
}
func (pe ParseError) column(lines []string) int {
var pos, col int
for i := range lines {
ll := len(lines[i]) + 1 // +1 for the removed newline
if pos+ll >= pe.Position.Start {
col = pe.Position.Start - pos
if col < 0 { // Should never happen, but just in case.
col = 0
}
break
}
pos += ll
}
return col
}
type (
errLexControl struct{ r rune }
errLexEscape struct{ r rune }
errLexUTF8 struct{ b byte }
errLexInvalidNum struct{ v string }
errLexInvalidDate struct{ v string }
errLexInlineTableNL struct{}
errLexStringNL struct{}
errParseRange struct {
i interface{} // int or float
size string // "int64", "uint16", etc.
}
errParseDuration struct{ d string }
)
func (e errLexControl) Error() string {
return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
}
func (e errLexControl) Usage() string { return "" }
func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
func (e errLexEscape) Usage() string { return usageEscape }
func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
func (e errLexUTF8) Usage() string { return "" }
func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) }
func (e errLexInvalidNum) Usage() string { return "" }
func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) }
func (e errLexInvalidDate) Usage() string { return "" }
func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
func (e errLexStringNL) Usage() string { return usageStringNewline }
func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
func (e errParseRange) Usage() string { return usageIntOverflow }
func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
func (e errParseDuration) Usage() string { return usageDuration }
const usageEscape = `
A '\' inside a "-delimited string is interpreted as an escape character.
The following escape sequences are supported:
\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
To prevent a '\' from being recognized as an escape character, use either:
- a ' or '''-delimited string; escape characters aren't processed in them; or
- write two backslashes to get a single backslash: '\\'.
If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
instead of '\' will usually also work: "C:/Users/martin".
`
const usageInlineNewline = `
Inline tables must always be on a single line:
table = {key = 42, second = 43}
It is invalid to split them over multiple lines like so:
# INVALID
table = {
key = 42,
second = 43
}
Use regular for this:
[table]
key = 42
second = 43
`
const usageStringNewline = `
Strings must always be on a single line, and cannot span more than one line:
# INVALID
string = "Hello,
world!"
Instead use """ or ''' to split strings over multiple lines:
string = """Hello,
world!"""
`
const usageIntOverflow = `
This number is too large; this may be an error in the TOML, but it can also be a
bug in the program that uses too small of an integer.
The maximum and minimum values are:
size lowest highest
int8 -128 127
int16 -32,768 32,767
int32 -2,147,483,648 2,147,483,647
int64 -9.2 × 10¹ 9.2 × 10¹
uint8 0 255
uint16 0 65535
uint32 0 4294967295
uint64 0 1.8 × 10¹
int refers to int32 on 32-bit systems and int64 on 64-bit systems.
`
const usageDuration = `
A duration must be as "number<unit>", without any spaces. Valid units are:
ns nanoseconds (billionth of a second)
us, µs microseconds (millionth of a second)
ms milliseconds (thousands of a second)
s seconds
m minutes
h hours
You can combine multiple units; for example "5m10s" for 5 minutes and 10
seconds.
`

36
vendor/github.com/BurntSushi/toml/internal/tz.go generated vendored Normal file
View file

@ -0,0 +1,36 @@
package internal
import "time"
// Timezones used for local datetime, date, and time TOML types.
//
// The exact way times and dates without a timezone should be interpreted is not
// well-defined in the TOML specification and left to the implementation. These
// defaults to current local timezone offset of the computer, but this can be
// changed by changing these variables before decoding.
//
// TODO:
// Ideally we'd like to offer people the ability to configure the used timezone
// by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit
// tricky: the reason we use three different variables for this is to support
// round-tripping without these specific TZ names we wouldn't know which
// format to use.
//
// There isn't a good way to encode this right now though, and passing this sort
// of information also ties in to various related issues such as string format
// encoding, encoding of comments, etc.
//
// So, for the time being, just put this in internal until we can write a good
// comprehensive API for doing all of this.
//
// The reason they're exported is because they're referred from in e.g.
// internal/tag.
//
// Note that this behaviour is valid according to the TOML spec as the exact
// behaviour is left up to implementations.
var (
localOffset = func() int { _, o := time.Now().Zone(); return o }()
LocalDatetime = time.FixedZone("datetime-local", localOffset)
LocalDate = time.FixedZone("date-local", localOffset)
LocalTime = time.FixedZone("time-local", localOffset)
)

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,40 @@
package toml
import "strings"
import (
"strings"
)
// MetaData allows access to meta information about TOML data that may not
// be inferrable via reflection. In particular, whether a key has been defined
// and the TOML type of a key.
// MetaData allows access to meta information about TOML data that's not
// accessible otherwise.
//
// It allows checking if a key is defined in the TOML data, whether any keys
// were undecoded, and the TOML type of a key.
type MetaData struct {
mapping map[string]interface{}
types map[string]tomlType
keys []Key
decoded map[string]bool
context Key // Used only during decoding.
keyInfo map[string]keyInfo
mapping map[string]interface{}
keys []Key
decoded map[string]struct{}
data []byte // Input file; for errors.
}
// IsDefined returns true if the key given exists in the TOML data. The key
// should be specified hierarchially. e.g.,
// IsDefined reports if the key exists in the TOML data.
//
// // access the TOML key 'a.b.c'
// IsDefined("a", "b", "c")
// The key should be specified hierarchically, for example to access the TOML
// key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive.
//
// IsDefined will return false if an empty key given. Keys are case sensitive.
// Returns false for an empty key.
func (md *MetaData) IsDefined(key ...string) bool {
if len(key) == 0 {
return false
}
var hash map[string]interface{}
var ok bool
var hashOrVal interface{} = md.mapping
var (
hash map[string]interface{}
ok bool
hashOrVal interface{} = md.mapping
)
for _, k := range key {
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
return false
@ -41,58 +48,20 @@ func (md *MetaData) IsDefined(key ...string) bool {
// Type returns a string representation of the type of the key specified.
//
// Type will return the empty string if given an empty key or a key that
// does not exist. Keys are case sensitive.
// Type will return the empty string if given an empty key or a key that does
// not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string {
fullkey := strings.Join(key, ".")
if typ, ok := md.types[fullkey]; ok {
return typ.typeString()
if ki, ok := md.keyInfo[Key(key).String()]; ok {
return ki.tomlType.typeString()
}
return ""
}
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
// to get values of this type.
type Key []string
func (k Key) String() string {
return strings.Join(k, ".")
}
func (k Key) maybeQuotedAll() string {
var ss []string
for i := range k {
ss = append(ss, k.maybeQuoted(i))
}
return strings.Join(ss, ".")
}
func (k Key) maybeQuoted(i int) string {
quote := false
for _, c := range k[i] {
if !isBareKeyChar(c) {
quote = true
break
}
}
if quote {
return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
}
return k[i]
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
newKey[len(k)] = piece
return newKey
}
// Keys returns a slice of every key in the TOML data, including key groups.
// Each key is itself a slice, where the first element is the top of the
// hierarchy and the last is the most specific.
//
// The list will have the same order as the keys appeared in the TOML data.
// Each key is itself a slice, where the first element is the top of the
// hierarchy and the last is the most specific. The list will have the same
// order as the keys appeared in the TOML data.
//
// All keys returned are non-empty.
func (md *MetaData) Keys() []Key {
@ -102,7 +71,7 @@ func (md *MetaData) Keys() []Key {
// Undecoded returns all keys that have not been decoded in the order in which
// they appear in the original TOML document.
//
// This includes keys that haven't been decoded because of a Primitive value.
// This includes keys that haven't been decoded because of a [Primitive] value.
// Once the Primitive value is decoded, the keys will be considered decoded.
//
// Also note that decoding into an empty interface will result in no decoding,
@ -113,9 +82,40 @@ func (md *MetaData) Keys() []Key {
func (md *MetaData) Undecoded() []Key {
undecoded := make([]Key, 0, len(md.keys))
for _, key := range md.keys {
if !md.decoded[key.String()] {
if _, ok := md.decoded[key.String()]; !ok {
undecoded = append(undecoded, key)
}
}
return undecoded
}
// Key represents any TOML key, including key groups. Use [MetaData.Keys] to get
// values of this type.
type Key []string
func (k Key) String() string {
ss := make([]string, len(k))
for i := range k {
ss[i] = k.maybeQuoted(i)
}
return strings.Join(ss, ".")
}
func (k Key) maybeQuoted(i int) string {
if k[i] == "" {
return `""`
}
for _, c := range k[i] {
if !isBareKeyChar(c, false) {
return `"` + dblQuotedReplacer.Replace(k[i]) + `"`
}
}
return k[i]
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
newKey[len(k)] = piece
return newKey
}

View file

@ -2,57 +2,80 @@ package toml
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/BurntSushi/toml/internal"
)
type parser struct {
mapping map[string]interface{}
types map[string]tomlType
lx *lexer
lx *lexer
context Key // Full key for the current hash in scope.
currentKey string // Base key name for everything except hashes.
pos Position // Current position in the TOML file.
tomlNext bool
// A list of keys in the order that they appear in the TOML data.
ordered []Key
ordered []Key // List of keys in the order that they appear in the TOML data.
// the full key for the current hash in scope
context Key
// the base key name for everything except hashes
currentKey string
// rough approximation of line number
approxLine int
// A map of 'key.group.names' to whether they were created implicitly.
implicits map[string]bool
keyInfo map[string]keyInfo // Map keyname → info about the TOML key.
mapping map[string]interface{} // Map keyname → key value.
implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names").
}
type parseError string
func (pe parseError) Error() string {
return string(pe)
type keyInfo struct {
pos Position
tomlType tomlType
}
func parse(data string) (p *parser, err error) {
_, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110")
defer func() {
if r := recover(); r != nil {
var ok bool
if err, ok = r.(parseError); ok {
if pErr, ok := r.(ParseError); ok {
pErr.input = data
err = pErr
return
}
panic(r)
}
}()
// Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString()
// which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add
// it anyway.
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16
data = data[2:]
} else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8
data = data[3:]
}
// Examine first few bytes for NULL bytes; this probably means it's a UTF-16
// file (second byte in surrogate pair being NULL). Again, do this here to
// avoid having to deal with UTF-8/16 stuff in the lexer.
ex := 6
if len(data) < 6 {
ex = len(data)
}
if i := strings.IndexRune(data[:ex], 0); i > -1 {
return nil, ParseError{
Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8",
Position: Position{Line: 1, Start: i, Len: 1},
Line: 1,
input: data,
}
}
p = &parser{
keyInfo: make(map[string]keyInfo),
mapping: make(map[string]interface{}),
types: make(map[string]tomlType),
lx: lex(data),
lx: lex(data, tomlNext),
ordered: make([]Key, 0),
implicits: make(map[string]bool),
implicits: make(map[string]struct{}),
tomlNext: tomlNext,
}
for {
item := p.next()
@ -65,20 +88,57 @@ func parse(data string) (p *parser, err error) {
return p, nil
}
func (p *parser) panicErr(it item, err error) {
panic(ParseError{
err: err,
Position: it.pos,
Line: it.pos.Len,
LastKey: p.current(),
})
}
func (p *parser) panicItemf(it item, format string, v ...interface{}) {
panic(ParseError{
Message: fmt.Sprintf(format, v...),
Position: it.pos,
Line: it.pos.Len,
LastKey: p.current(),
})
}
func (p *parser) panicf(format string, v ...interface{}) {
msg := fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
p.approxLine, p.current(), fmt.Sprintf(format, v...))
panic(parseError(msg))
panic(ParseError{
Message: fmt.Sprintf(format, v...),
Position: p.pos,
Line: p.pos.Line,
LastKey: p.current(),
})
}
func (p *parser) next() item {
it := p.lx.nextItem()
//fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val)
if it.typ == itemError {
p.panicf("%s", it.val)
if it.err != nil {
panic(ParseError{
Position: it.pos,
Line: it.pos.Line,
LastKey: p.current(),
err: it.err,
})
}
p.panicItemf(it, "%s", it.val)
}
return it
}
func (p *parser) nextPos() item {
it := p.next()
p.pos = it.pos
return it
}
func (p *parser) bug(format string, v ...interface{}) {
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
}
@ -97,44 +157,60 @@ func (p *parser) assertEqual(expected, got itemType) {
func (p *parser) topLevel(item item) {
switch item.typ {
case itemCommentStart:
p.approxLine = item.line
case itemCommentStart: // # ..
p.expect(itemText)
case itemTableStart:
kg := p.next()
p.approxLine = kg.line
case itemTableStart: // [ .. ]
name := p.nextPos()
var key Key
for ; kg.typ != itemTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() {
key = append(key, p.keyString(name))
}
p.assertEqual(itemTableEnd, kg.typ)
p.assertEqual(itemTableEnd, name.typ)
p.establishContext(key, false)
p.setType("", tomlHash)
p.addContext(key, false)
p.setType("", tomlHash, item.pos)
p.ordered = append(p.ordered, key)
case itemArrayTableStart:
kg := p.next()
p.approxLine = kg.line
case itemArrayTableStart: // [[ .. ]]
name := p.nextPos()
var key Key
for ; kg.typ != itemArrayTableEnd && kg.typ != itemEOF; kg = p.next() {
key = append(key, p.keyString(kg))
for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() {
key = append(key, p.keyString(name))
}
p.assertEqual(itemArrayTableEnd, kg.typ)
p.assertEqual(itemArrayTableEnd, name.typ)
p.establishContext(key, true)
p.setType("", tomlArrayHash)
p.addContext(key, true)
p.setType("", tomlArrayHash, item.pos)
p.ordered = append(p.ordered, key)
case itemKeyStart:
kname := p.next()
p.approxLine = kname.line
p.currentKey = p.keyString(kname)
case itemKeyStart: // key = ..
outerContext := p.context
/// Read all the key parts (e.g. 'a' and 'b' in 'a.b')
k := p.nextPos()
var key Key
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
key = append(key, p.keyString(k))
}
p.assertEqual(itemKeyEnd, k.typ)
val, typ := p.value(p.next())
p.setValue(p.currentKey, val)
p.setType(p.currentKey, typ)
/// The current key is the last part.
p.currentKey = key[len(key)-1]
/// All the other parts (if any) are the context; need to set each part
/// as implicit.
context := key[:len(key)-1]
for i := range context {
p.addImplicitContext(append(p.context, context[i:i+1]...))
}
p.ordered = append(p.ordered, p.context.add(p.currentKey))
/// Set value.
vItem := p.next()
val, typ := p.value(vItem, false)
p.set(p.currentKey, val, typ, vItem.pos)
/// Remove the context we added (preserving any context from [tbl] lines).
p.context = outerContext
p.currentKey = ""
default:
p.bug("Unexpected type at top level: %s", item.typ)
@ -148,180 +224,271 @@ func (p *parser) keyString(it item) string {
return it.val
case itemString, itemMultilineString,
itemRawString, itemRawMultilineString:
s, _ := p.value(it)
s, _ := p.value(it, false)
return s.(string)
default:
p.bug("Unexpected key type: %s", it.typ)
panic("unreachable")
}
panic("unreachable")
}
var datetimeRepl = strings.NewReplacer(
"z", "Z",
"t", "T",
" ", "T")
// value translates an expected value from the lexer into a Go value wrapped
// as an empty interface.
func (p *parser) value(it item) (interface{}, tomlType) {
func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) {
switch it.typ {
case itemString:
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it)
case itemMultilineString:
trimmed := stripFirstNewline(stripEscapedWhitespace(it.val))
return p.replaceEscapes(trimmed), p.typeOfPrimitive(it)
return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it)
case itemRawString:
return it.val, p.typeOfPrimitive(it)
case itemRawMultilineString:
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
case itemInteger:
return p.valueInteger(it)
case itemFloat:
return p.valueFloat(it)
case itemBool:
switch it.val {
case "true":
return true, p.typeOfPrimitive(it)
case "false":
return false, p.typeOfPrimitive(it)
default:
p.bug("Expected boolean value, but got '%s'.", it.val)
}
p.bug("Expected boolean value, but got '%s'.", it.val)
case itemInteger:
if !numUnderscoresOK(it.val) {
p.panicf("Invalid integer %q: underscores must be surrounded by digits",
it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseInt(val, 10, 64)
if err != nil {
// Distinguish integer values. Normally, it'd be a bug if the lexer
// provides an invalid integer, but it's possible that the number is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Integer '%s' is out of the range of 64-bit "+
"signed integers.", it.val)
} else {
p.bug("Expected integer value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemFloat:
parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r {
case '.', 'e', 'E':
return true
}
return false
})
for _, part := range parts {
if !numUnderscoresOK(part) {
p.panicf("Invalid float %q: underscores must be "+
"surrounded by digits", it.val)
}
}
if !numPeriodsOK(it.val) {
// As a special case, numbers like '123.' or '1.e2',
// which are valid as far as Go/strconv are concerned,
// must be rejected because TOML says that a fractional
// part consists of '.' followed by 1+ digits.
p.panicf("Invalid float %q: '.' must be followed "+
"by one or more digits", it.val)
}
val := strings.Replace(it.val, "_", "", -1)
num, err := strconv.ParseFloat(val, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Float '%s' is out of the range of 64-bit "+
"IEEE-754 floating-point numbers.", it.val)
} else {
p.panicf("Invalid float value: %q", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemDatetime:
var t time.Time
var ok bool
var err error
for _, format := range []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02",
} {
t, err = time.ParseInLocation(format, it.val, time.Local)
if err == nil {
ok = true
break
}
}
if !ok {
p.panicf("Invalid TOML Datetime: %q.", it.val)
}
return t, p.typeOfPrimitive(it)
return p.valueDatetime(it)
case itemArray:
array := make([]interface{}, 0)
types := make([]tomlType, 0)
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
val, typ := p.value(it)
array = append(array, val)
types = append(types, typ)
}
return array, p.typeOfArray(types)
return p.valueArray(it)
case itemInlineTableStart:
var (
hash = make(map[string]interface{})
outerContext = p.context
outerKey = p.currentKey
)
p.context = append(p.context, p.currentKey)
p.currentKey = ""
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
if it.typ != itemKeyStart {
p.bug("Expected key start but instead found %q, around line %d",
it.val, p.approxLine)
}
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
// retrieve key
k := p.next()
p.approxLine = k.line
kname := p.keyString(k)
// retrieve value
p.currentKey = kname
val, typ := p.value(p.next())
// make sure we keep metadata up to date
p.setType(kname, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
hash[kname] = val
}
p.context = outerContext
p.currentKey = outerKey
return hash, tomlHash
return p.valueInlineTable(it, parentIsArray)
default:
p.bug("Unexpected value type: %s", it.typ)
}
p.bug("Unexpected value type: %s", it.typ)
panic("unreachable")
}
func (p *parser) valueInteger(it item) (interface{}, tomlType) {
if !numUnderscoresOK(it.val) {
p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val)
}
if numHasLeadingZero(it.val) {
p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val)
}
num, err := strconv.ParseInt(it.val, 0, 64)
if err != nil {
// Distinguish integer values. Normally, it'd be a bug if the lexer
// provides an invalid integer, but it's possible that the number is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
p.panicErr(it, errParseRange{i: it.val, size: "int64"})
} else {
p.bug("Expected integer value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
}
func (p *parser) valueFloat(it item) (interface{}, tomlType) {
parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r {
case '.', 'e', 'E':
return true
}
return false
})
for _, part := range parts {
if !numUnderscoresOK(part) {
p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val)
}
}
if len(parts) > 0 && numHasLeadingZero(parts[0]) {
p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val)
}
if !numPeriodsOK(it.val) {
// As a special case, numbers like '123.' or '1.e2',
// which are valid as far as Go/strconv are concerned,
// must be rejected because TOML says that a fractional
// part consists of '.' followed by 1+ digits.
p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val)
}
val := strings.Replace(it.val, "_", "", -1)
if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does.
val = "nan"
}
num, err := strconv.ParseFloat(val, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
p.panicErr(it, errParseRange{i: it.val, size: "float64"})
} else {
p.panicItemf(it, "Invalid float value: %q", it.val)
}
}
return num, p.typeOfPrimitive(it)
}
var dtTypes = []struct {
fmt string
zone *time.Location
next bool
}{
{time.RFC3339Nano, time.Local, false},
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false},
{"2006-01-02", internal.LocalDate, false},
{"15:04:05.999999999", internal.LocalTime, false},
// tomlNext
{"2006-01-02T15:04Z07:00", time.Local, true},
{"2006-01-02T15:04", internal.LocalDatetime, true},
{"15:04", internal.LocalTime, true},
}
func (p *parser) valueDatetime(it item) (interface{}, tomlType) {
it.val = datetimeRepl.Replace(it.val)
var (
t time.Time
ok bool
err error
)
for _, dt := range dtTypes {
if dt.next && !p.tomlNext {
continue
}
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
if err == nil {
ok = true
break
}
}
if !ok {
p.panicItemf(it, "Invalid TOML Datetime: %q.", it.val)
}
return t, p.typeOfPrimitive(it)
}
func (p *parser) valueArray(it item) (interface{}, tomlType) {
p.setType(p.currentKey, tomlArray, it.pos)
var (
types []tomlType
// Initialize to a non-nil empty slice. This makes it consistent with
// how S = [] decodes into a non-nil slice inside something like struct
// { S []string }. See #338
array = []interface{}{}
)
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
val, typ := p.value(it, true)
array = append(array, val)
types = append(types, typ)
// XXX: types isn't used here, we need it to record the accurate type
// information.
//
// Not entirely sure how to best store this; could use "key[0]",
// "key[1]" notation, or maybe store it on the Array type?
_ = types
}
return array, tomlArray
}
func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) {
var (
hash = make(map[string]interface{})
outerContext = p.context
outerKey = p.currentKey
)
p.context = append(p.context, p.currentKey)
prevContext := p.context
p.currentKey = ""
p.addImplicit(p.context)
p.addContext(p.context, parentIsArray)
/// Loop over all table key/value pairs.
for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() {
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
/// Read all key parts.
k := p.nextPos()
var key Key
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
key = append(key, p.keyString(k))
}
p.assertEqual(itemKeyEnd, k.typ)
/// The current key is the last part.
p.currentKey = key[len(key)-1]
/// All the other parts (if any) are the context; need to set each part
/// as implicit.
context := key[:len(key)-1]
for i := range context {
p.addImplicitContext(append(p.context, context[i:i+1]...))
}
p.ordered = append(p.ordered, p.context.add(p.currentKey))
/// Set the value.
val, typ := p.value(p.next(), false)
p.set(p.currentKey, val, typ, it.pos)
hash[p.currentKey] = val
/// Restore context.
p.context = prevContext
}
p.context = outerContext
p.currentKey = outerKey
return hash, tomlHash
}
// numHasLeadingZero checks if this number has leading zeroes, allowing for '0',
// +/- signs, and base prefixes.
func numHasLeadingZero(s string) bool {
if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x
return true
}
if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' {
return true
}
return false
}
// numUnderscoresOK checks whether each underscore in s is surrounded by
// characters that are not underscores.
func numUnderscoresOK(s string) bool {
switch s {
case "nan", "+nan", "-nan", "inf", "-inf", "+inf":
return true
}
accept := false
for _, r := range s {
if r == '_' {
if !accept {
return false
}
accept = false
continue
}
accept = true
// isHexadecimal is a superset of all the permissable characters
// surrounding an underscore.
accept = isHexadecimal(r)
}
return accept
}
@ -338,13 +505,12 @@ func numPeriodsOK(s string) bool {
return !period
}
// establishContext sets the current context of the parser,
// where the context is either a hash or an array of hashes. Which one is
// set depends on the value of the `array` parameter.
// Set the current context of the parser, where the context is either a hash or
// an array of hashes, depending on the value of the `array` parameter.
//
// Establishing the context also makes sure that the key isn't a duplicate, and
// will create implicit hashes automatically.
func (p *parser) establishContext(key Key, array bool) {
func (p *parser) addContext(key Key, array bool) {
var ok bool
// Always start at the top level and drill down for our context.
@ -383,7 +549,7 @@ func (p *parser) establishContext(key Key, array bool) {
// list of tables for it.
k := key[len(key)-1]
if _, ok := hashContext[k]; !ok {
hashContext[k] = make([]map[string]interface{}, 0, 5)
hashContext[k] = make([]map[string]interface{}, 0, 4)
}
// Add a new table. But make sure the key hasn't already been used
@ -391,8 +557,7 @@ func (p *parser) establishContext(key Key, array bool) {
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
hashContext[k] = append(hash, make(map[string]interface{}))
} else {
p.panicf("Key '%s' was already created and cannot be used as "+
"an array.", keyContext)
p.panicf("Key '%s' was already created and cannot be used as an array.", key)
}
} else {
p.setValue(key[len(key)-1], make(map[string]interface{}))
@ -400,15 +565,22 @@ func (p *parser) establishContext(key Key, array bool) {
p.context = append(p.context, key[len(key)-1])
}
// set calls setValue and setType.
func (p *parser) set(key string, val interface{}, typ tomlType, pos Position) {
p.setValue(key, val)
p.setType(key, typ, pos)
}
// setValue sets the given key to the given value in the current context.
// It will make sure that the key hasn't already been defined, account for
// implicit key groups.
func (p *parser) setValue(key string, value interface{}) {
var tmpHash interface{}
var ok bool
hash := p.mapping
keyContext := make(Key, 0)
var (
tmpHash interface{}
ok bool
hash = p.mapping
keyContext Key
)
for _, k := range p.context {
keyContext = append(keyContext, k)
if tmpHash, ok = hash[k]; !ok {
@ -422,24 +594,26 @@ func (p *parser) setValue(key string, value interface{}) {
case map[string]interface{}:
hash = t
default:
p.bug("Expected hash to have type 'map[string]interface{}', but "+
"it has '%T' instead.", tmpHash)
p.panicf("Key '%s' has already been defined.", keyContext)
}
}
keyContext = append(keyContext, key)
if _, ok := hash[key]; ok {
// Typically, if the given key has already been set, then we have
// to raise an error since duplicate keys are disallowed. However,
// it's possible that a key was previously defined implicitly. In this
// case, it is allowed to be redefined concretely. (See the
// `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.)
// Normally redefining keys isn't allowed, but the key could have been
// defined implicitly and it's allowed to be redefined concretely. (See
// the `valid/implicit-and-explicit-after.toml` in toml-test)
//
// But we have to make sure to stop marking it as an implicit. (So that
// another redefinition provokes an error.)
//
// Note that since it has already been defined (as a hash), we don't
// want to overwrite it. So our business is done.
if p.isArray(keyContext) {
p.removeImplicit(keyContext)
hash[key] = value
return
}
if p.isImplicit(keyContext) {
p.removeImplicit(keyContext)
return
@ -449,41 +623,37 @@ func (p *parser) setValue(key string, value interface{}) {
// key, which is *always* wrong.
p.panicf("Key '%s' has already been defined.", keyContext)
}
hash[key] = value
}
// setType sets the type of a particular value at a given key.
// It should be called immediately AFTER setValue.
// setType sets the type of a particular value at a given key. It should be
// called immediately AFTER setValue.
//
// Note that if `key` is empty, then the type given will be applied to the
// current context (which is either a table or an array of tables).
func (p *parser) setType(key string, typ tomlType) {
func (p *parser) setType(key string, typ tomlType, pos Position) {
keyContext := make(Key, 0, len(p.context)+1)
for _, k := range p.context {
keyContext = append(keyContext, k)
}
keyContext = append(keyContext, p.context...)
if len(key) > 0 { // allow type setting for hashes
keyContext = append(keyContext, key)
}
p.types[keyContext.String()] = typ
// Special case to make empty keys ("" = 1) work.
// Without it it will set "" rather than `""`.
// TODO: why is this needed? And why is this only needed here?
if len(keyContext) == 0 {
keyContext = Key{""}
}
p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos}
}
// addImplicit sets the given Key as having been created implicitly.
func (p *parser) addImplicit(key Key) {
p.implicits[key.String()] = true
}
// removeImplicit stops tagging the given key as having been implicitly
// created.
func (p *parser) removeImplicit(key Key) {
p.implicits[key.String()] = false
}
// isImplicit returns true if the key group pointed to by the key was created
// implicitly.
func (p *parser) isImplicit(key Key) bool {
return p.implicits[key.String()]
}
// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and
// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly).
func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} }
func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) }
func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok }
func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray }
func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) }
// current returns the full key name of the current context.
func (p *parser) current() string {
@ -497,24 +667,67 @@ func (p *parser) current() string {
}
func stripFirstNewline(s string) string {
if len(s) == 0 || s[0] != '\n' {
return s
if len(s) > 0 && s[0] == '\n' {
return s[1:]
}
return s[1:]
if len(s) > 1 && s[0] == '\r' && s[1] == '\n' {
return s[2:]
}
return s
}
func stripEscapedWhitespace(s string) string {
esc := strings.Split(s, "\\\n")
if len(esc) > 1 {
for i := 1; i < len(esc); i++ {
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
// stripEscapedNewlines removes whitespace after line-ending backslashes in
// multiline strings.
//
// A line-ending backslash is an unescaped \ followed only by whitespace until
// the next newline. After a line-ending backslash, all whitespace is removed
// until the next non-whitespace character.
func (p *parser) stripEscapedNewlines(s string) string {
var b strings.Builder
var i int
for {
ix := strings.Index(s[i:], `\`)
if ix < 0 {
b.WriteString(s)
return b.String()
}
i += ix
if len(s) > i+1 && s[i+1] == '\\' {
// Escaped backslash.
i += 2
continue
}
// Scan until the next non-whitespace.
j := i + 1
whitespaceLoop:
for ; j < len(s); j++ {
switch s[j] {
case ' ', '\t', '\r', '\n':
default:
break whitespaceLoop
}
}
if j == i+1 {
// Not a whitespace escape.
i++
continue
}
if !strings.Contains(s[i:j], "\n") {
// This is not a line-ending backslash.
// (It's a bad escape sequence, but we can let
// replaceEscapes catch it.)
i++
continue
}
b.WriteString(s[:i])
s = s[j:]
i = 0
}
return strings.Join(esc, "")
}
func (p *parser) replaceEscapes(str string) string {
var replaced []rune
func (p *parser) replaceEscapes(it item, str string) string {
replaced := make([]rune, 0, len(str))
s := []byte(str)
r := 0
for r < len(s) {
@ -532,7 +745,8 @@ func (p *parser) replaceEscapes(str string) string {
switch s[r] {
default:
p.bug("Expected valid escape code after \\, but got %q.", s[r])
return ""
case ' ', '\t':
p.panicItemf(it, "invalid escape: '\\%c'", s[r])
case 'b':
replaced = append(replaced, rune(0x0008))
r += 1
@ -548,24 +762,35 @@ func (p *parser) replaceEscapes(str string) string {
case 'r':
replaced = append(replaced, rune(0x000D))
r += 1
case 'e':
if p.tomlNext {
replaced = append(replaced, rune(0x001B))
r += 1
}
case '"':
replaced = append(replaced, rune(0x0022))
r += 1
case '\\':
replaced = append(replaced, rune(0x005C))
r += 1
case 'x':
if p.tomlNext {
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+3])
replaced = append(replaced, escaped)
r += 3
}
case 'u':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+5])
replaced = append(replaced, escaped)
r += 5
case 'U':
// At this point, we know we have a Unicode escape of the form
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
// for us.)
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+9])
replaced = append(replaced, escaped)
r += 9
}
@ -573,20 +798,14 @@ func (p *parser) replaceEscapes(str string) string {
return string(replaced)
}
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
func (p *parser) asciiEscapeToUnicode(it item, bs []byte) rune {
s := string(bs)
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
if err != nil {
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
"lexer claims it's OK: %s", s, err)
p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err)
}
if !utf8.ValidRune(rune(hex)) {
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s)
}
return rune(hex)
}
func isStringType(ty itemType) bool {
return ty == itemString || ty == itemMultilineString ||
ty == itemRawString || ty == itemRawMultilineString
}

View file

@ -1 +0,0 @@
au BufWritePost *.go silent!make tags > /dev/null 2>&1

View file

@ -70,8 +70,8 @@ func typeFields(t reflect.Type) []field {
next := []field{{typ: t}}
// Count of queued names for current level and the next.
count := map[reflect.Type]int{}
nextCount := map[reflect.Type]int{}
var count map[reflect.Type]int
var nextCount map[reflect.Type]int
// Types already visited at an earlier level.
visited := map[reflect.Type]bool{}

View file

@ -16,7 +16,7 @@ func typeEqual(t1, t2 tomlType) bool {
return t1.typeString() == t2.typeString()
}
func typeIsHash(t tomlType) bool {
func typeIsTable(t tomlType) bool {
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
}
@ -68,24 +68,3 @@ func (p *parser) typeOfPrimitive(lexItem item) tomlType {
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
panic("unreachable")
}
// typeOfArray returns a tomlType for an array given a list of types of its
// values.
//
// In the current spec, if an array is homogeneous, then its type is always
// "Array". If the array is not homogeneous, an error is generated.
func (p *parser) typeOfArray(types []tomlType) tomlType {
// Empty arrays are cool.
if len(types) == 0 {
return tomlArray
}
theType := types[0]
for _, t := range types[1:] {
if !typeEqual(theType, t) {
p.panicf("Array contains values of type '%s' and '%s', but "+
"arrays must be homogeneous.", theType, t)
}
}
return tomlArray
}

View file

@ -27,3 +27,4 @@ _testmain.go
*.prof
*.iml
.idea
.DS_Store

View file

@ -1,20 +0,0 @@
language: go
sudo: false
go:
- "1.9.x"
- "1.10.x"
- "1.11.x"
- "1.12.x"
- "1.13.x"
before_install:
- go get -t ./...
matrix:
allow_failures:
- go: 1.13.x
script:
- GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./...

View file

@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.13.0](https://github.com/andygrunwald/go-jira/compare/v1.11.1...v1.13.0) (2020-10-25)
### Features
* add AddRemoteLink method ([f200e15](https://github.com/andygrunwald/go-jira/commit/f200e158b997a303db081cbbc5a9d8ad5d89566d)), closes [/developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2](https://github.com/andygrunwald//developer.atlassian.com/cloud/jira/platform/rest/v2//issues/api-rest-api-2)
* Add Names support on Issue struct ([#278](https://github.com/andygrunwald/go-jira/issues/278)) ([1fc10e0](https://github.com/andygrunwald/go-jira/commit/1fc10e0606784f745673ccc4d8d706c36f385a7a))
* Extend Makefile for more source code quality targets ([5e52236](https://github.com/andygrunwald/go-jira/commit/5e5223631a29d10a13e598318a6abe47384e2982))
* **context:** Add support for context package ([e1f4265](https://github.com/andygrunwald/go-jira/commit/e1f4265e2b467b938fe0c095caf6d36f3136d2ff))
* **issues:** Add GetEditMeta on issue ([a783764](https://github.com/andygrunwald/go-jira/commit/a783764b52dc890773658ddd0483a9d0393e385d)), closes [/docs.atlassian.com/DAC/rest/jira/6.1.html#d2e1364](https://github.com/andygrunwald//docs.atlassian.com/DAC/rest/jira/6.1.html/issues/d2e1364)
* **IssueService:** allow empty JQL ([#268](https://github.com/andygrunwald/go-jira/issues/268)) ([4b91cf2](https://github.com/andygrunwald/go-jira/commit/4b91cf2b135355de7ecee41727c3e65f4e7067bc))
* **project:** Add cronjob to check for stale issues ([#287](https://github.com/andygrunwald/go-jira/issues/287)) ([2096b04](https://github.com/andygrunwald/go-jira/commit/2096b04e52b434c1fb1c841bab487a94674a271e))
* **project:** Add GitHub Actions testing workflow ([#289](https://github.com/andygrunwald/go-jira/issues/289)) ([80c0282](https://github.com/andygrunwald/go-jira/commit/80c02828ca9e4eb0e4a1877275baae14d330a2d9)), closes [#290](https://github.com/andygrunwald/go-jira/issues/290)
* **project:** Add workflow to greet new contributors ([#288](https://github.com/andygrunwald/go-jira/issues/288)) ([c357b61](https://github.com/andygrunwald/go-jira/commit/c357b61a40f62a919ebd94a555390958f99c8db7))
### Bug Fixes
* change millisecond time format ([8c77107](https://github.com/andygrunwald/go-jira/commit/8c77107df3757c4ec5eae6e9d7c018618e708bfa))
* paging with load balancer going to endless loop ([19d3fc0](https://github.com/andygrunwald/go-jira/commit/19d3fc0aecde547ffe1ab547c5ffb6c7972d387c)), closes [#260](https://github.com/andygrunwald/go-jira/issues/260)
* **issue:** IssueService.Search() with a not empty JQL triggers 400 bad request ([#292](https://github.com/andygrunwald/go-jira/issues/292)) ([8b64c7f](https://github.com/andygrunwald/go-jira/commit/8b64c7f005fbceb11fa43a7aff3de61eb3166fca)), closes [#291](https://github.com/andygrunwald/go-jira/issues/291)
* **IssueService.GetWatchers:** UserService.GetByAccountID support accountId params ([436469b](https://github.com/andygrunwald/go-jira/commit/436469b62d4d62037f380b38c918a13f4a5f0ab2))
* **product:** Make product naming consistent, rename JIRA to Jira ([#286](https://github.com/andygrunwald/go-jira/issues/286)) ([146229d](https://github.com/andygrunwald/go-jira/commit/146229d2ab58a3fb128ddc8dcbe03aff72e20857)), closes [#284](https://github.com/andygrunwald/go-jira/issues/284)
* **tests:** Fix TestIssueService_PostAttachment unit test ([f6b1dca](https://github.com/andygrunwald/go-jira/commit/f6b1dcafcfdd8fe69f842b1053c4030da6c97c7f))
* removing the use of username field in searching for users ([#297](https://github.com/andygrunwald/go-jira/issues/297)) ([f50cb07](https://github.com/andygrunwald/go-jira/commit/f50cb07b297d79138b13e5ab49ea33965d32f5c1))
## [1.12.0](https://github.com/andygrunwald/go-jira/compare/v1.11.1...v1.12.0) (2019-12-14)
@ -76,6 +102,3 @@ All notable changes to this project will be documented in this file. See [standa
* Add ResolutionService to retrieve resolutions ([fb1ce22](https://github.com/andygrunwald/go-jira/commit/fb1ce22))
* Add status category constants ([6223ddd](https://github.com/andygrunwald/go-jira/commit/6223ddd))
* Add StatusCategory GetList ([049a756](https://github.com/andygrunwald/go-jira/commit/049a756))

View file

@ -1,2 +1,25 @@
test:
go test -v ./...
.DEFAULT_GOAL := help
.PHONY: help
help: ## Outputs the help.
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: test
test: ## Runs all unit, integration and example tests.
go test -race -v ./...
.PHONY: vet
vet: ## Runs go vet (to detect suspicious constructs).
go vet ./...
.PHONY: fmt
fmt: ## Runs go fmt (to check for go coding guidelines).
gofmt -d -s .
.PHONY: staticcheck
staticcheck: ## Runs static analysis to prevend bugs, foster code simplicity, performance and editor integration.
go install honnef.co/go/tools/cmd/staticcheck@2022.1
staticcheck ./...
.PHONY: all
all: test vet fmt staticcheck ## Runs all source code quality targets (like test, vet, fmt, staticcheck)

View file

@ -1,15 +0,0 @@
# PR Description
_What does this fix or add?_
# Checklist
* [ ] Tests added
* [ ] Good Path
* [ ] Error Path
* [ ] Commits follow conventions described here:
* [ ] [https://conventionalcommits.org/en/v1.0.0-beta.4/#summary](https://conventionalcommits.org/en/v1.0.0-beta.4/#summary)
* [ ] [https://chris.beams.io/posts/git-commit/#seven-rules](https://chris.beams.io/posts/git-commit/#seven-rules)
* [ ] Commits are squashed such that
* [ ] There is 1 commit per isolated change
* [ ] I've not made extraneous commits/changes that are unrelated to my change.

View file

@ -1,26 +1,29 @@
# go-jira
[![GoDoc](https://godoc.org/github.com/andygrunwald/go-jira?status.svg)](https://godoc.org/github.com/andygrunwald/go-jira)
[![Build Status](https://travis-ci.org/andygrunwald/go-jira.svg?branch=master)](https://travis-ci.org/andygrunwald/go-jira)
[![Build Status](https://github.com/andygrunwald/go-jira/actions/workflows/testing.yml/badge.svg)](https://github.com/andygrunwald/go-jira/actions/workflows/testing.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/andygrunwald/go-jira)](https://goreportcard.com/report/github.com/andygrunwald/go-jira)
[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira).
[Go](https://golang.org/) client library for [Atlassian Jira](https://www.atlassian.com/software/jira).
![Go client library for Atlassian JIRA](./img/logo_small.png "Go client library for Atlassian JIRA.")
![Go client library for Atlassian Jira](./img/logo_small.png "Go client library for Atlassian Jira.")
## Features
* Authentication (HTTP Basic, OAuth, Session Cookie)
* Create and retrieve issues
* Create and retrieve issue transitions (status updates)
* Call every API endpoint of the JIRA, even if it is not directly implemented in this library
* Call every API endpoint of the Jira, even if it is not directly implemented in this library
This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JIRA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/).
This package is not Jira API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of Jira have a look at [latest Jira REST API documentation](https://docs.atlassian.com/jira/REST/latest/).
## Requirements
* Go >= 1.8
* JIRA v6.3.4 & v7.1.2.
* Go >= 1.14
* Jira v6.3.4 & v7.1.2.
Note that we also run our tests against 1.13, though only the last two versions
of Go are officially supported.
## Installation
@ -52,7 +55,7 @@ go test -v ./...
Please have a look at the [GoDoc documentation](https://godoc.org/github.com/andygrunwald/go-jira) for a detailed API description.
The [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package.
The [latest Jira REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package.
## Examples
@ -68,7 +71,7 @@ package main
import (
"fmt"
"github.com/andygrunwald/go-jira"
jira "github.com/andygrunwald/go-jira"
)
func main() {
@ -92,9 +95,11 @@ an `http.Client`. That client can then be passed into the `NewClient` function
For convenience, capability for basic and cookie-based authentication is included in the main library.
#### Basic auth example
#### Token (Jira on Atlassian Cloud)
A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory. **It's worth noting that using passwords in basic auth is now deprecated and will be removed.** Jira gives you the ability to [create tokens now.](https://confluence.atlassian.com/cloud/api-tokens-938839638.html)
Token-based authentication uses the basic authentication scheme, with a user-generated API token in place of a user's password. You can generate a token for your user [here](https://id.atlassian.com/manage-profile/security/api-tokens). Additional information about Atlassian Cloud API tokens can be found [here](https://confluence.atlassian.com/cloud/api-tokens-938839638.html).
A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory.
```go
func main() {
@ -111,14 +116,15 @@ func main() {
}
```
#### Authenticate with session cookie [DEPRECATED]
#### Basic (self-hosted Jira)
JIRA [deprecated this authentication method.](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/) It's not longer available for use.
Password-based API authentication works for self-hosted Jira **only**, and has been [deprecated for users of Atlassian Cloud](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/).
The above token authentication example may be used, substituting a user's password for a generated token.
#### Authenticate with OAuth
If you want to connect via OAuth to your JIRA Cloud instance checkout the [example of using OAuth authentication with JIRA in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus).
If you want to connect via OAuth to your Jira Cloud instance checkout the [example of using OAuth authentication with Jira in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus).
For more details have a look at the [issue #56](https://github.com/andygrunwald/go-jira/issues/56).
@ -173,11 +179,62 @@ func main() {
}
```
### Change an issue status
This is how one can change an issue status. In this example, we change the issue from "To Do" to "In Progress."
```go
package main
import (
"fmt"
"github.com/andygrunwald/go-jira"
)
func main() {
base := "https://my.jira.com"
tp := jira.BasicAuthTransport{
Username: "username",
Password: "token",
}
jiraClient, err := jira.NewClient(tp.Client(), base)
if err != nil {
panic(err)
}
issue, _, _ := jiraClient.Issue.Get("FART-1", nil)
currentStatus := issue.Fields.Status.Name
fmt.Printf("Current status: %s\n", currentStatus)
var transitionID string
possibleTransitions, _, _ := jiraClient.Issue.GetTransitions("FART-1")
for _, v := range possibleTransitions {
if v.Name == "In Progress" {
transitionID = v.ID
break
}
}
jiraClient.Issue.DoTransition("FART-1", transitionID)
issue, _, _ = jiraClient.Issue.Get(testIssueID, nil)
fmt.Printf("Status after transition: %+v\n", issue.Fields.Status.Name)
}
```
### Get all the issues for JQL with Pagination
Jira API has limit on maxResults it can return. You may have a usecase where you need to get all issues for given JQL.
This example shows reference implementation of GetAllIssues function which does pagination on Jira API to get all the issues for given JQL
please look at [Pagination Example](https://github.com/andygrunwald/go-jira/blob/master/examples/pagination/main.go)
### Call a not implemented API endpoint
Not all API endpoints of the JIRA API are implemented into *go-jira*.
Not all API endpoints of the Jira API are implemented into *go-jira*.
But you can call them anyway:
Lets get all public projects of [Atlassian`s JIRA instance](https://jira.atlassian.com/).
Lets get all public projects of [Atlassian`s Jira instance](https://jira.atlassian.com/).
```go
package main
@ -209,7 +266,7 @@ func main() {
// ...
// BAM: Bamboo
// BAMJ: Bamboo JIRA Plugin
// BAMJ: Bamboo Jira Plugin
// CLOV: Clover
// CONF: Confluence
// ...
@ -218,7 +275,7 @@ func main() {
## Implementations
* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The JIRA Ticket Checker
* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The Jira Ticket Checker
## Code structure
@ -226,7 +283,7 @@ The code structure of this package was inspired by [google/go-github](https://gi
There is one main part (the client).
Based on this main client the other endpoints, like Issues or Authentication are extracted in services. E.g. `IssueService` or `AuthenticationService`.
These services own a responsibility of the single endpoints / usecases of JIRA.
These services own a responsibility of the single endpoints / usecases of Jira.
## Contribution
@ -258,7 +315,7 @@ You can read more about them at https://developer.atlassian.com/blog/2016/04/clo
## Releasing
Install `standard-version`
Install [standard-version](https://github.com/conventional-changelog/standard-version)
```bash
npm i -g standard-version
```

View file

@ -1,6 +1,7 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
@ -14,9 +15,9 @@ const (
authTypeSession = 2
)
// AuthenticationService handles authentication for the JIRA instance / API.
// AuthenticationService handles authentication for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
type AuthenticationService struct {
client *Client
@ -30,7 +31,7 @@ type AuthenticationService struct {
password string
}
// Session represents a Session JSON response by the JIRA API.
// Session represents a Session JSON response by the Jira API.
type Session struct {
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
@ -47,16 +48,16 @@ type Session struct {
Cookies []*http.Cookie
}
// AcquireSessionCookie creates a new session for a user in JIRA.
// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
// AcquireSessionCookieWithContext creates a new session for a user in Jira.
// Once a session has been successfully created it can be used to access any of Jira's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
// The header will by automatically applied to every API request.
// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
//
// Deprecated: Use CookieAuthTransport instead
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
func (s *AuthenticationService) AcquireSessionCookieWithContext(ctx context.Context, username, password string) (bool, error) {
apiEndpoint := "rest/auth/1/session"
body := struct {
Username string `json:"username"`
@ -66,7 +67,7 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
password,
}
req, err := s.client.NewRequest("POST", apiEndpoint, body)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, body)
if err != nil {
return false, err
}
@ -79,10 +80,10 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
}
if err != nil {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). %s", err)
}
if resp != nil && resp.StatusCode != 200 {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
}
s.client.session = session
@ -91,7 +92,14 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
return true, nil
}
// SetBasicAuth sets username and password for the basic auth against the JIRA instance.
// AcquireSessionCookie wraps AcquireSessionCookieWithContext using the background context.
//
// Deprecated: Use CookieAuthTransport instead
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
return s.AcquireSessionCookieWithContext(context.Background(), username, password)
}
// SetBasicAuth sets username and password for the basic auth against the Jira instance.
//
// Deprecated: Use BasicAuthTransport instead
func (s *AuthenticationService) SetBasicAuth(username, password string) {
@ -100,7 +108,7 @@ func (s *AuthenticationService) SetBasicAuth(username, password string) {
s.authType = authTypeBasic
}
// Authenticated reports if the current Client has authentication details for JIRA
// Authenticated reports if the current Client has authentication details for Jira
func (s *AuthenticationService) Authenticated() bool {
if s != nil {
if s.authType == authTypeSession {
@ -113,29 +121,30 @@ func (s *AuthenticationService) Authenticated() bool {
return false
}
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
// LogoutWithContext logs out the current user that has been authenticated and the session in the client is destroyed.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
//
// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
// client anymore
func (s *AuthenticationService) Logout() error {
func (s *AuthenticationService) LogoutWithContext(ctx context.Context) error {
if s.authType != authTypeSession || s.client.session == nil {
return fmt.Errorf("no user is authenticated")
}
apiEndpoint := "rest/auth/1/session"
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return fmt.Errorf("Creating the request to log the user out failed : %s", err)
return fmt.Errorf("creating the request to log the user out failed : %s", err)
}
resp, err := s.client.Do(req, nil)
if err != nil {
return fmt.Errorf("Error sending the logout request: %s", err)
return fmt.Errorf("error sending the logout request: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode)
return fmt.Errorf("the logout was unsuccessful with status %d", resp.StatusCode)
}
// If logout successful, delete session
@ -145,43 +154,55 @@ func (s *AuthenticationService) Logout() error {
}
// GetCurrentUser gets the details of the current user.
// Logout wraps LogoutWithContext using the background context.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
func (s *AuthenticationService) GetCurrentUser() (*Session, error) {
// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
// client anymore
func (s *AuthenticationService) Logout() error {
return s.LogoutWithContext(context.Background())
}
// GetCurrentUserWithContext gets the details of the current user.
//
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
func (s *AuthenticationService) GetCurrentUserWithContext(ctx context.Context) (*Session, error) {
if s == nil {
return nil, fmt.Errorf("AUthenticaiton Service is not instantiated")
return nil, fmt.Errorf("authentication Service is not instantiated")
}
if s.authType != authTypeSession || s.client.session == nil {
return nil, fmt.Errorf("No user is authenticated yet")
return nil, fmt.Errorf("no user is authenticated yet")
}
apiEndpoint := "rest/auth/1/session"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("Could not create request for getting user info : %s", err)
return nil, fmt.Errorf("could not create request for getting user info : %s", err)
}
resp, err := s.client.Do(req, nil)
if err != nil {
return nil, fmt.Errorf("Error sending request to get user info : %s", err)
return nil, fmt.Errorf("error sending request to get user info : %s", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("getting user info failed with status : %d", resp.StatusCode)
}
ret := new(Session)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Couldn't read body from the response : %s", err)
return nil, fmt.Errorf("couldn't read body from the response : %s", err)
}
err = json.Unmarshal(data, &ret)
if err != nil {
return nil, fmt.Errorf("Could not unmarshall received user info : %s", err)
return nil, fmt.Errorf("could not unmarshall received user info : %s", err)
}
return ret, nil
}
// GetCurrentUser wraps GetCurrentUserWithContext using the background context.
func (s *AuthenticationService) GetCurrentUser() (*Session, error) {
return s.GetCurrentUserWithContext(context.Background())
}

View file

@ -1,14 +1,15 @@
package jira
import (
"context"
"fmt"
"strconv"
"time"
)
// BoardService handles Agile Boards for the JIRA instance / API.
// BoardService handles Agile Boards for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/
// Jira API docs: https://docs.atlassian.com/jira-software/REST/server/
type BoardService struct {
client *Client
}
@ -22,7 +23,7 @@ type BoardsList struct {
Values []Board `json:"values" structs:"values"`
}
// Board represents a JIRA agile board
// Board represents a Jira agile board
type Board struct {
ID int `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
@ -62,7 +63,7 @@ type SprintsList struct {
Values []Sprint `json:"values" structs:"values"`
}
// Sprint represents a sprint on JIRA agile board
// Sprint represents a sprint on Jira agile board
type Sprint struct {
ID int `json:"id" structs:"id"`
Name string `json:"name" structs:"name"`
@ -116,6 +117,8 @@ type BoardConfigurationColumnConfig struct {
type BoardConfigurationColumn struct {
Name string `json:"name"`
Status []BoardConfigurationColumnStatus `json:"statuses"`
Min int `json:"min,omitempty"`
Max int `json:"max,omitempty"`
}
// BoardConfigurationColumnStatus represents a status in the column configuration
@ -124,16 +127,16 @@ type BoardConfigurationColumnStatus struct {
Self string `json:"self"`
}
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
// GetAllBoardsWithContext will returns all boards. This only includes boards that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards
func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards
func (s *BoardService) GetAllBoardsWithContext(ctx context.Context, opt *BoardListOptions) (*BoardsList, *Response, error) {
apiEndpoint := "rest/agile/1.0/board"
url, err := addOptions(apiEndpoint, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", url, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -148,13 +151,18 @@ func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Respon
return boards, resp, err
}
// GetBoard will returns the board for the given boardID.
// GetAllBoards wraps GetAllBoardsWithContext using the background context.
func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) {
return s.GetAllBoardsWithContext(context.Background(), opt)
}
// GetBoardWithContext will returns the board for the given boardID.
// This board will only be returned if the user has permission to view it.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard
func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard
func (s *BoardService) GetBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -169,17 +177,22 @@ func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
return board, resp, nil
}
// CreateBoard creates a new board. Board name, type and filter Id is required.
// GetBoard wraps GetBoardWithContext using the background context.
func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
return s.GetBoardWithContext(context.Background(), boardID)
}
// CreateBoardWithContext creates a new board. Board name, type and filter Id is required.
// name - Must be less than 255 characters.
// type - Valid values: scrum, kanban
// filterId - Id of a filter that the user has permissions to view.
// Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private
// board will be created instead (remember that board sharing depends on the filter sharing).
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard
func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard
func (s *BoardService) CreateBoardWithContext(ctx context.Context, board *Board) (*Board, *Response, error) {
apiEndpoint := "rest/agile/1.0/board"
req, err := s.client.NewRequest("POST", apiEndpoint, board)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, board)
if err != nil {
return nil, nil, err
}
@ -194,12 +207,18 @@ func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
return responseBoard, resp, nil
}
// DeleteBoard will delete an agile board.
// CreateBoard wraps CreateBoardWithContext using the background context.
func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
return s.CreateBoardWithContext(context.Background(), board)
}
// DeleteBoardWithContext will delete an agile board.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard
func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard
// Caller must close resp.Body
func (s *BoardService) DeleteBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -211,11 +230,17 @@ func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
return nil, resp, err
}
// GetAllSprints will return all sprints from a board, for a given board Id.
// DeleteBoard wraps DeleteBoardWithContext using the background context.
// Caller must close resp.Body
func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
return s.DeleteBoardWithContext(context.Background(), boardID)
}
// GetAllSprintsWithContext will return all sprints from a board, for a given board Id.
// This only includes sprints that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprintsWithContext(ctx context.Context, boardID string) ([]Sprint, *Response, error) {
id, err := strconv.Atoi(boardID)
if err != nil {
return nil, nil, err
@ -229,17 +254,22 @@ func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error
return result.Values, response, nil
}
// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options
// GetAllSprints wraps GetAllSprintsWithContext using the background context.
func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) {
return s.GetAllSprintsWithContext(context.Background(), boardID)
}
// GetAllSprintsWithOptionsWithContext will return sprints from a board, for a given board Id and filtering options
// This only includes sprints that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprintsWithOptionsWithContext(ctx context.Context, boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID)
url, err := addOptions(apiEndpoint, options)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", url, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -253,12 +283,17 @@ func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSpri
return result, resp, err
}
// GetBoardConfiguration will return a board configuration for a given board Id
// GetAllSprintsWithOptions wraps GetAllSprintsWithOptionsWithContext using the background context.
func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
return s.GetAllSprintsWithOptionsWithContext(context.Background(), boardID, options)
}
// GetBoardConfigurationWithContext will return a board configuration for a given board Id
// Jira API docs:https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-boardId-configuration-get
func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) {
func (s *BoardService) GetBoardConfigurationWithContext(ctx context.Context, boardID int) (*BoardConfiguration, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/configuration", boardID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -273,3 +308,8 @@ func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration,
return result, resp, err
}
// GetBoardConfiguration wraps GetBoardConfigurationWithContext using the background context.
func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) {
return s.GetBoardConfigurationWithContext(context.Background(), boardID)
}

View file

@ -1,13 +1,14 @@
package jira
// ComponentService handles components for the JIRA instance / API.
//
// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component
import "context"
// ComponentService handles components for the Jira instance / API.//
// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component
type ComponentService struct {
client *Client
}
// CreateComponentOptions are passed to the ComponentService.Create function to create a new JIRA component
// CreateComponentOptions are passed to the ComponentService.Create function to create a new Jira component
type CreateComponentOptions struct {
Name string `json:"name,omitempty" structs:"name,omitempty"`
Description string `json:"description,omitempty" structs:"description,omitempty"`
@ -19,10 +20,10 @@ type CreateComponentOptions struct {
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
}
// Create creates a new JIRA component based on the given options.
func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
// CreateWithContext creates a new Jira component based on the given options.
func (s *ComponentService) CreateWithContext(ctx context.Context, options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
apiEndpoint := "rest/api/2/component"
req, err := s.client.NewRequest("POST", apiEndpoint, options)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, options)
if err != nil {
return nil, nil, err
}
@ -36,3 +37,8 @@ func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComp
return component, resp, nil
}
// Create wraps CreateWithContext using the background context.
func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
return s.CreateWithContext(context.Background(), options)
}

72
vendor/github.com/andygrunwald/go-jira/customer.go generated vendored Normal file
View file

@ -0,0 +1,72 @@
package jira
import (
"context"
"net/http"
)
// CustomerService handles ServiceDesk customers for the Jira instance / API.
type CustomerService struct {
client *Client
}
// Customer represents a ServiceDesk customer.
type Customer struct {
AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"`
DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"`
Active *bool `json:"active,omitempty" structs:"active,omitempty"`
TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"`
Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"`
}
// CustomerListOptions is the query options for listing customers.
type CustomerListOptions struct {
Query string `url:"query,omitempty"`
Start int `url:"start,omitempty"`
Limit int `url:"limit,omitempty"`
}
// CustomerList is a page of customers.
type CustomerList struct {
Values []Customer `json:"values,omitempty" structs:"values,omitempty"`
Start int `json:"start,omitempty" structs:"start,omitempty"`
Limit int `json:"limit,omitempty" structs:"limit,omitempty"`
IsLast bool `json:"isLastPage,omitempty" structs:"isLastPage,omitempty"`
Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"`
}
// CreateWithContext creates a ServiceDesk customer.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-customer/#api-rest-servicedeskapi-customer-post
func (c *CustomerService) CreateWithContext(ctx context.Context, email, displayName string) (*Customer, *Response, error) {
const apiEndpoint = "rest/servicedeskapi/customer"
payload := struct {
Email string `json:"email"`
DisplayName string `json:"displayName"`
}{
Email: email,
DisplayName: displayName,
}
req, err := c.client.NewRequestWithContext(ctx, http.MethodPost, apiEndpoint, payload)
if err != nil {
return nil, nil, err
}
responseCustomer := new(Customer)
resp, err := c.client.Do(req, responseCustomer)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}
return responseCustomer, resp, nil
}
// Create wraps CreateWithContext using the background context.
func (c *CustomerService) Create(email, displayName string) (*Customer, *Response, error) {
return c.CreateWithContext(context.Background(), email, displayName)
}

View file

@ -10,7 +10,7 @@ import (
"github.com/pkg/errors"
)
// Error message from JIRA
// Error message from Jira
// See https://docs.atlassian.com/jira/REST/cloud/#error-responses
type Error struct {
HTTPError error
@ -34,13 +34,13 @@ func NewJiraError(resp *Response, httpError error) error {
if strings.HasPrefix(contentType, "application/json") {
err = json.Unmarshal(body, &jerr)
if err != nil {
httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error())
httpError = errors.Wrap(errors.New("could not parse JSON"), httpError.Error())
return errors.Wrap(err, httpError.Error())
}
} else {
if httpError == nil {
return fmt.Errorf("Got Response Status %s:%s", resp.Status, string(body))
}
if httpError == nil {
return fmt.Errorf("got response status %s:%s", resp.Status, string(body))
}
return errors.Wrap(httpError, fmt.Sprintf("%s: %s", resp.Status, string(body)))
}

View file

@ -1,13 +1,15 @@
package jira
// FieldService handles fields for the JIRA instance / API.
import "context"
// FieldService handles fields for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field
type FieldService struct {
client *Client
}
// Field represents a field of a JIRA issue.
// Field represents a field of a Jira issue.
type Field struct {
ID string `json:"id,omitempty" structs:"id,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
@ -19,17 +21,22 @@ type Field struct {
Schema FieldSchema `json:"schema,omitempty" structs:"schema,omitempty"`
}
// FieldSchema represents a schema of a Jira field.
// Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-fields/#api-rest-api-2-field-get
type FieldSchema struct {
Type string `json:"type,omitempty" structs:"type,omitempty"`
System string `json:"system,omitempty" structs:"system,omitempty"`
Type string `json:"type,omitempty" structs:"type,omitempty"`
Items string `json:"items,omitempty" structs:"items,omitempty"`
Custom string `json:"custom,omitempty" structs:"custom,omitempty"`
System string `json:"system,omitempty" structs:"system,omitempty"`
CustomID int64 `json:"customId,omitempty" structs:"customId,omitempty"`
}
// GetList gets all fields from JIRA
// GetListWithContext gets all fields from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get
func (s *FieldService) GetList() ([]Field, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get
func (s *FieldService) GetListWithContext(ctx context.Context) ([]Field, *Response, error) {
apiEndpoint := "rest/api/2/field"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -41,3 +48,8 @@ func (s *FieldService) GetList() ([]Field, *Response, error) {
}
return fieldList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *FieldService) GetList() ([]Field, *Response, error) {
return s.GetListWithContext(context.Background())
}

View file

@ -1,11 +1,15 @@
package jira
import "github.com/google/go-querystring/query"
import "fmt"
import (
"context"
"fmt"
// FilterService handles fields for the JIRA instance / API.
"github.com/google/go-querystring/query"
)
// FilterService handles fields for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter
type FilterService struct {
client *Client
}
@ -116,23 +120,21 @@ type FilterSearchOptions struct {
Expand string `url:"expand,omitempty"`
}
// GetList retrieves all filters from Jira
func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
// GetListWithContext retrieves all filters from Jira
func (fs *FilterService) GetListWithContext(ctx context.Context) ([]*Filter, *Response, error) {
options := &GetQueryOptions{}
apiEndpoint := "rest/api/2/filter"
req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
if options != nil {
q, err := query.Values(options)
if err != nil {
return nil, nil, err
}
req.URL.RawQuery = q.Encode()
q, err := query.Values(options)
if err != nil {
return nil, nil, err
}
req.URL.RawQuery = q.Encode()
filters := []*Filter{}
resp, err := fs.client.Do(req, &filters)
@ -143,10 +145,15 @@ func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
return filters, resp, err
}
// GetFavouriteList retrieves the user's favourited filters from Jira
func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
// GetList wraps GetListWithContext using the background context.
func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
return fs.GetListWithContext(context.Background())
}
// GetFavouriteListWithContext retrieves the user's favourited filters from Jira
func (fs *FilterService) GetFavouriteListWithContext(ctx context.Context) ([]*Filter, *Response, error) {
apiEndpoint := "rest/api/2/filter/favourite"
req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -159,10 +166,15 @@ func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
return filters, resp, err
}
// Get retrieves a single Filter from Jira
func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
// GetFavouriteList wraps GetFavouriteListWithContext using the background context.
func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
return fs.GetFavouriteListWithContext(context.Background())
}
// GetWithContext retrieves a single Filter from Jira
func (fs *FilterService) GetWithContext(ctx context.Context, filterID int) (*Filter, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/filter/%d", filterID)
req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -176,16 +188,21 @@ func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
return filter, resp, err
}
// GetMyFilters retrieves the my Filters.
// Get wraps GetWithContext using the background context.
func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
return fs.GetWithContext(context.Background(), filterID)
}
// GetMyFiltersWithContext retrieves the my Filters.
//
// https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-my-get
func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) {
func (fs *FilterService) GetMyFiltersWithContext(ctx context.Context, opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) {
apiEndpoint := "rest/api/3/filter/my"
url, err := addOptions(apiEndpoint, opts)
if err != nil {
return nil, nil, err
}
req, err := fs.client.NewRequest("GET", url, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -199,16 +216,21 @@ func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter
return filters, resp, nil
}
// Search will search for filter according to the search options
// GetMyFilters wraps GetMyFiltersWithContext using the background context.
func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) {
return fs.GetMyFiltersWithContext(context.Background(), opts)
}
// SearchWithContext will search for filter according to the search options
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get
func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get
func (fs *FilterService) SearchWithContext(ctx context.Context, opt *FilterSearchOptions) (*FiltersList, *Response, error) {
apiEndpoint := "rest/api/3/filter/search"
url, err := addOptions(apiEndpoint, opt)
if err != nil {
return nil, nil, err
}
req, err := fs.client.NewRequest("GET", url, nil)
req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, nil, err
}
@ -222,3 +244,8 @@ func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Respon
return filters, resp, err
}
// Search wraps SearchWithContext using the background context.
func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) {
return fs.SearchWithContext(context.Background(), opt)
}

View file

@ -1,13 +1,14 @@
package jira
import (
"context"
"fmt"
"net/url"
)
// GroupService handles Groups for the JIRA instance / API.
// GroupService handles Groups for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group
// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group
type GroupService struct {
client *Client
}
@ -21,7 +22,7 @@ type groupMembersResult struct {
Members []GroupMember `json:"values"`
}
// Group represents a JIRA group
// Group represents a Jira group
type Group struct {
ID string `json:"id"`
Title string `json:"title"`
@ -58,16 +59,16 @@ type GroupSearchOptions struct {
IncludeInactiveUsers bool
}
// Get returns a paginated list of users who are members of the specified group and its subgroups.
// GetWithContext returns a paginated list of users who are members of the specified group and its subgroups.
// Users in the page are ordered by user names.
// User of this resource is required to have sysadmin or admin permissions.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
//
// WARNING: This API only returns the first page of group members
func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
func (s *GroupService) GetWithContext(ctx context.Context, name string) ([]GroupMember, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name))
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -81,12 +82,17 @@ func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
return group.Members, resp, nil
}
// GetWithOptions returns a paginated list of members of the specified group and its subgroups.
// Get wraps GetWithContext using the background context.
func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
return s.GetWithContext(context.Background(), name)
}
// GetWithOptionsWithContext returns a paginated list of members of the specified group and its subgroups.
// Users in the page are ordered by user names.
// User of this resource is required to have sysadmin or admin permissions.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
func (s *GroupService) GetWithOptionsWithContext(ctx context.Context, name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
var apiEndpoint string
if options == nil {
apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name))
@ -99,7 +105,7 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions)
options.IncludeInactiveUsers,
)
}
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -112,16 +118,21 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions)
return group.Members, resp, nil
}
// Add adds user to group
// GetWithOptions wraps GetWithOptionsWithContext using the background context.
func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
return s.GetWithOptionsWithContext(context.Background(), name, options)
}
// AddWithContext adds user to group
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup
func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup
func (s *GroupService) AddWithContext(ctx context.Context, groupname string, username string) (*Group, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname)
var user struct {
Name string `json:"name"`
}
user.Name = username
req, err := s.client.NewRequest("POST", apiEndpoint, &user)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, &user)
if err != nil {
return nil, nil, err
}
@ -136,12 +147,18 @@ func (s *GroupService) Add(groupname string, username string) (*Group, *Response
return responseGroup, resp, nil
}
// Remove removes user from group
// Add wraps AddWithContext using the background context.
func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) {
return s.AddWithContext(context.Background(), groupname, username)
}
// RemoveWithContext removes user from group
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup
func (s *GroupService) Remove(groupname string, username string) (*Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup
// Caller must close resp.Body
func (s *GroupService) RemoveWithContext(ctx context.Context, groupname string, username string) (*Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, err
}
@ -154,3 +171,9 @@ func (s *GroupService) Remove(groupname string, username string) (*Response, err
return resp, nil
}
// Remove wraps RemoveWithContext using the background context.
// Caller must close resp.Body
func (s *GroupService) Remove(groupname string, username string) (*Response, error) {
return s.RemoveWithContext(context.Background(), groupname, username)
}

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,25 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
)
// IssueLinkTypeService handles issue link types for the JIRA instance / API.
// IssueLinkTypeService handles issue link types for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Issue-link-types
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Issue-link-types
type IssueLinkTypeService struct {
client *Client
}
// GetList gets all of the issue link types from JIRA.
// GetListWithContext gets all of the issue link types from Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get
func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get
func (s *IssueLinkTypeService) GetListWithContext(ctx context.Context) ([]IssueLinkType, *Response, error) {
apiEndpoint := "rest/api/2/issueLinkType"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -31,12 +32,17 @@ func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) {
return linkTypeList, resp, nil
}
// Get gets info of a specific issue link type from JIRA.
// GetList wraps GetListWithContext using the background context.
func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) {
return s.GetListWithContext(context.Background())
}
// GetWithContext gets info of a specific issue link type from Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get
func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get
func (s *IssueLinkTypeService) GetWithContext(ctx context.Context, ID string) (*IssueLinkType, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID)
req, err := s.client.NewRequest("GET", apiEndPoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
if err != nil {
return nil, nil, err
}
@ -49,12 +55,17 @@ func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error)
return linkType, resp, nil
}
// Create creates an issue link type in JIRA.
// Get wraps GetWithContext using the background context.
func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) {
return s.GetWithContext(context.Background(), ID)
}
// CreateWithContext creates an issue link type in Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post
func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post
func (s *IssueLinkTypeService) CreateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
apiEndpoint := "/rest/api/2/issueLinkType"
req, err := s.client.NewRequest("POST", apiEndpoint, linkType)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, linkType)
if err != nil {
return nil, nil, err
}
@ -68,23 +79,29 @@ func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType,
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
e := fmt.Errorf("Could not read the returned data")
e := fmt.Errorf("could not read the returned data")
return nil, resp, NewJiraError(resp, e)
}
err = json.Unmarshal(data, responseLinkType)
if err != nil {
e := fmt.Errorf("Could no unmarshal the data into struct")
e := fmt.Errorf("could no unmarshal the data into struct")
return nil, resp, NewJiraError(resp, e)
}
return linkType, resp, nil
}
// Update updates an issue link type. The issue is found by key.
// Create wraps CreateWithContext using the background context.
func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
return s.CreateWithContext(context.Background(), linkType)
}
// UpdateWithContext updates an issue link type. The issue is found by key.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put
func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put
// Caller must close resp.Body
func (s *IssueLinkTypeService) UpdateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", linkType.ID)
req, err := s.client.NewRequest("PUT", apiEndpoint, linkType)
req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, linkType)
if err != nil {
return nil, nil, err
}
@ -96,12 +113,19 @@ func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType,
return &ret, resp, nil
}
// Delete deletes an issue link type based on provided ID.
// Update wraps UpdateWithContext using the background context.
// Caller must close resp.Body
func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) {
return s.UpdateWithContext(context.Background(), linkType)
}
// DeleteWithContext deletes an issue link type based on provided ID.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete
func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete
// Caller must close resp.Body
func (s *IssueLinkTypeService) DeleteWithContext(ctx context.Context, ID string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, err
}
@ -109,3 +133,9 @@ func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) {
resp, err := s.client.Do(req, nil)
return resp, err
}
// Delete wraps DeleteWithContext using the background context.
// Caller must close resp.Body
func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) {
return s.DeleteWithContext(context.Background(), ID)
}

View file

@ -2,6 +2,7 @@ package jira
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@ -14,7 +15,7 @@ import (
"strings"
"time"
"github.com/dgrijalva/jwt-go"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/google/go-querystring/query"
"github.com/pkg/errors"
)
@ -25,7 +26,7 @@ type httpClient interface {
Do(request *http.Request) (response *http.Response, err error)
}
// A Client manages communication with the JIRA API.
// A Client manages communication with the Jira API.
type Client struct {
// HTTP client used to communicate with the API.
client httpClient
@ -36,7 +37,7 @@ type Client struct {
// Session storage if the user authenticates with a Session cookie
session *Session
// Services used for talking to different parts of the JIRA API.
// Services used for talking to different parts of the Jira API.
Authentication *AuthenticationService
Issue *IssueService
Project *ProjectService
@ -55,15 +56,19 @@ type Client struct {
PermissionScheme *PermissionSchemeService
Status *StatusService
IssueLinkType *IssueLinkTypeService
Organization *OrganizationService
ServiceDesk *ServiceDeskService
Customer *CustomerService
Request *RequestService
}
// NewClient returns a new JIRA API client.
// NewClient returns a new Jira API client.
// If a nil httpClient is provided, http.DefaultClient will be used.
// To use API methods which require authentication you can follow the preferred solution and
// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
// As an alternative you can use Session Cookie based authentication provided by this package as well.
// See https://docs.atlassian.com/jira/REST/latest/#authentication
// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash.
// baseURL is the HTTP endpoint of your Jira instance and should always be specified with a trailing slash.
func NewClient(httpClient httpClient, baseURL string) (*Client, error) {
if httpClient == nil {
httpClient = http.DefaultClient
@ -101,14 +106,18 @@ func NewClient(httpClient httpClient, baseURL string) (*Client, error) {
c.PermissionScheme = &PermissionSchemeService{client: c}
c.Status = &StatusService{client: c}
c.IssueLinkType = &IssueLinkTypeService{client: c}
c.Organization = &OrganizationService{client: c}
c.ServiceDesk = &ServiceDeskService{client: c}
c.Customer = &CustomerService{client: c}
c.Request = &RequestService{client: c}
return c, nil
}
// NewRawRequest creates an API request.
// NewRawRequestWithContext creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Allows using an optional native io.Reader for sourcing the request body.
func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
func (c *Client) NewRawRequestWithContext(ctx context.Context, method, urlStr string, body io.Reader) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
@ -118,7 +127,7 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req
u := c.baseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), body)
req, err := newRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, err
}
@ -143,10 +152,15 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req
return req, nil
}
// NewRequest creates an API request.
// NewRawRequest wraps NewRawRequestWithContext using the background context.
func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
return c.NewRawRequestWithContext(context.Background(), method, urlStr, body)
}
// NewRequestWithContext creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// If specified, the value pointed to by body is JSON encoded and included as the request body.
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
func (c *Client) NewRequestWithContext(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
@ -165,7 +179,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
}
}
req, err := http.NewRequest(method, u.String(), buf)
req, err := newRequestWithContext(ctx, method, u.String(), buf)
if err != nil {
return nil, err
}
@ -190,6 +204,11 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
return req, nil
}
// NewRequest wraps NewRequestWithContext using the background context.
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
return c.NewRequestWithContext(context.Background(), method, urlStr, body)
}
// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opt interface{}) (string, error) {
@ -212,10 +231,10 @@ func addOptions(s string, opt interface{}) (string, error) {
return u.String(), nil
}
// NewMultiPartRequest creates an API request including a multi-part file.
// NewMultiPartRequestWithContext creates an API request including a multi-part file.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// If specified, the value pointed to by buf is a multipart form.
func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
func (c *Client) NewMultiPartRequestWithContext(ctx context.Context, method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
@ -225,7 +244,7 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
u := c.baseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), buf)
req, err := newRequestWithContext(ctx, method, u.String(), buf)
if err != nil {
return nil, err
}
@ -251,6 +270,11 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
return req, nil
}
// NewMultiPartRequest wraps NewMultiPartRequestWithContext using the background context.
func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
return c.NewMultiPartRequestWithContext(context.Background(), method, urlStr, buf)
}
// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
@ -279,13 +303,13 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if it has a status code outside the 200 range.
// The caller is responsible to analyze the response body.
// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes).
// The body can contain JSON (if the error is intended) or xml (sometimes Jira just failes).
func CheckResponse(r *http.Response) error {
if c := r.StatusCode; 200 <= c && c <= 299 {
return nil
}
err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
err := fmt.Errorf("request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
return err
}
@ -295,7 +319,7 @@ func (c *Client) GetBaseURL() url.URL {
return *c.baseURL
}
// Response represents JIRA API response. It wraps http.Response returned from
// Response represents Jira API response. It wraps http.Response returned from
// API and provides information about paging.
type Response struct {
*http.Response
@ -324,7 +348,6 @@ func (r *Response) populatePageValues(v interface{}) {
r.MaxResults = value.MaxResults
r.Total = value.Total
}
return
}
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
@ -363,13 +386,84 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
return http.DefaultTransport
}
// BearerAuthTransport is a http.RoundTripper that authenticates all requests
// using Jira's bearer (oauth 2.0 (3lo)) based authentication.
type BearerAuthTransport struct {
Token string
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}
// RoundTrip implements the RoundTripper interface. We just add the
// bearer token and return the RoundTripper for this transport type.
func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract
req2.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.Token))
return t.transport().RoundTrip(req2)
}
// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication. This is a nice little bit of sugar
// so we can just get the client instead of creating the client in the calling code.
// If it's necessary to send more information on client init, the calling code can
// always skip this and set the transport itself.
func (t *BearerAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}
func (t *BearerAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// PATAuthTransport is an http.RoundTripper that authenticates all requests
// using the Personal Access Token specified.
// See here for more info: https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html
type PATAuthTransport struct {
// Token is the key that was provided by Jira when creating the Personal Access Token.
Token string
// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}
// RoundTrip implements the RoundTripper interface. We just add the
// basic auth and return the RoundTripper for this transport type.
func (t *PATAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract
req2.Header.Set("Authorization", "Bearer "+t.Token)
return t.transport().RoundTrip(req2)
}
// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication. This is a nice little bit of sugar
// so we can just get the client instead of creating the client in the calling code.
// If it's necessary to send more information on client init, the calling code can
// always skip this and set the transport itself.
func (t *PATAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}
func (t *PATAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// CookieAuthTransport is an http.RoundTripper that authenticates all requests
// using Jira's cookie-based authentication.
//
// Note that it is generally preferable to use HTTP BASIC authentication with the REST API.
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
// However, this resource may be used to mimic the behaviour of Jira's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
type CookieAuthTransport struct {
Username string
Password string
@ -425,6 +519,7 @@ func (t *CookieAuthTransport) setSessionObject() error {
if err != nil {
return err
}
defer resp.Body.Close()
t.SessionObject = resp.Cookies()
return nil
@ -464,7 +559,7 @@ func (t *CookieAuthTransport) transport() http.RoundTripper {
//
// NOTE: this form of auth should be used by add-ons installed from the Atlassian marketplace.
//
// JIRA docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt
// Jira docs: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt
// Examples in other languages:
// https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/d44a8e7a4649e4f23edaa784402655fda7c816ea/lib/atlassian/jwt.rb
// https://bitbucket.org/atlassian/atlassian-jwt-py/src/master/atlassian_jwt/url_utils.py

View file

@ -1,6 +1,7 @@
package jira
import (
"context"
"fmt"
"strings"
@ -14,6 +15,11 @@ type CreateMetaInfo struct {
Projects []*MetaProject `json:"projects,omitempty"`
}
// EditMetaInfo contains information about fields and their attributed to edit a ticket.
type EditMetaInfo struct {
Fields tcontainer.MarshalMap `json:"fields,omitempty"`
}
// MetaProject is the meta information about a project returned from createmeta api
type MetaProject struct {
Expand string `json:"expand,omitempty"`
@ -42,16 +48,21 @@ type MetaIssueType struct {
Fields tcontainer.MarshalMap `json:"fields,omitempty"`
}
// GetCreateMeta makes the api call to get the meta information required to create a ticket
func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"})
// GetCreateMetaWithContext makes the api call to get the meta information required to create a ticket
func (s *IssueService) GetCreateMetaWithContext(ctx context.Context, projectkeys string) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithOptionsWithContext(ctx, &GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"})
}
// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey
func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
// GetCreateMeta wraps GetCreateMetaWithContext using the background context.
func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithContext(context.Background(), projectkeys)
}
// GetCreateMetaWithOptionsWithContext makes the api call to get the meta information without requiring to have a projectKey
func (s *IssueService) GetCreateMetaWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
apiEndpoint := "rest/api/2/issue/createmeta"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -73,11 +84,40 @@ func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*Crea
return meta, resp, nil
}
// GetCreateMetaWithOptions wraps GetCreateMetaWithOptionsWithContext using the background context.
func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
return s.GetCreateMetaWithOptionsWithContext(context.Background(), options)
}
// GetEditMetaWithContext makes the api call to get the edit meta information for an issue
func (s *IssueService) GetEditMetaWithContext(ctx context.Context, issue *Issue) (*EditMetaInfo, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/issue/%s/editmeta", issue.Key)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
meta := new(EditMetaInfo)
resp, err := s.client.Do(req, meta)
if err != nil {
return nil, resp, err
}
return meta, resp, nil
}
// GetEditMeta wraps GetEditMetaWithContext using the background context.
func (s *IssueService) GetEditMeta(issue *Issue) (*EditMetaInfo, *Response, error) {
return s.GetEditMetaWithContext(context.Background(), issue)
}
// GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil.
// The comparison of the name is case insensitive.
func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject {
for _, m := range m.Projects {
if strings.ToLower(m.Name) == strings.ToLower(name) {
if strings.EqualFold(m.Name, name) {
return m
}
}
@ -88,7 +128,7 @@ func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject {
// The comparison of the name is case insensitive.
func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject {
for _, m := range m.Projects {
if strings.ToLower(m.Key) == strings.ToLower(key) {
if strings.EqualFold(m.Key, key) {
return m
}
}
@ -99,7 +139,7 @@ func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject {
// The comparison of the name is case insensitive
func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType {
for _, m := range p.IssueTypes {
if strings.ToLower(m.Name) == strings.ToLower(name) {
if strings.EqualFold(m.Name, name) {
return m
}
}
@ -175,7 +215,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo
for name := range mandatory {
requiredFields = append(requiredFields, name)
}
return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields)
return false, fmt.Errorf("required field not found in provided jira.fields. Required are: %#v", requiredFields)
}
}
@ -186,7 +226,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo
for name := range all {
availableFields = append(availableFields, name)
}
return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields)
return false, fmt.Errorf("fields in jira.fields are not available in jira. Available are: %#v", availableFields)
}
}

397
vendor/github.com/andygrunwald/go-jira/organization.go generated vendored Normal file
View file

@ -0,0 +1,397 @@
package jira
import (
"context"
"fmt"
)
// OrganizationService handles Organizations for the Jira instance / API.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/
type OrganizationService struct {
client *Client
}
// OrganizationCreationDTO is DTO for creat organization API
type OrganizationCreationDTO struct {
Name string `json:"name,omitempty" structs:"name,omitempty"`
}
// SelfLink Stores REST API URL to the organization.
type SelfLink struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
}
// Organization contains Organization data
type Organization struct {
ID string `json:"id,omitempty" structs:"id,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"`
}
// OrganizationUsersDTO contains organization user ids
type OrganizationUsersDTO struct {
AccountIds []string `json:"accountIds,omitempty" structs:"accountIds,omitempty"`
}
// PagedDTO is response of a paged list
type PagedDTO struct {
Size int `json:"size,omitempty" structs:"size,omitempty"`
Start int `json:"start,omitempty" structs:"start,omitempty"`
Limit int `limit:"size,omitempty" structs:"limit,omitempty"`
IsLastPage bool `json:"isLastPage,omitempty" structs:"isLastPage,omitempty"`
Values []interface{} `values:"isLastPage,omitempty" structs:"values,omitempty"`
Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"`
}
// PropertyKey contains Property key details.
type PropertyKey struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
}
// PropertyKeys contains an array of PropertyKey
type PropertyKeys struct {
Keys []PropertyKey `json:"keys,omitempty" structs:"keys,omitempty"`
}
// GetAllOrganizationsWithContext returns a list of organizations in
// the Jira Service Management instance.
// Use this method when you want to present a list
// of organizations or want to locate an organization
// by name.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-group-organization
func (s *OrganizationService) GetAllOrganizationsWithContext(ctx context.Context, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization?start=%d&limit=%d", start, limit)
if accountID != "" {
apiEndPoint += fmt.Sprintf("&accountId=%s", accountID)
}
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
v := new(PagedDTO)
resp, err := s.client.Do(req, v)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return v, resp, nil
}
// GetAllOrganizations wraps GetAllOrganizationsWithContext using the background context.
func (s *OrganizationService) GetAllOrganizations(start int, limit int, accountID string) (*PagedDTO, *Response, error) {
return s.GetAllOrganizationsWithContext(context.Background(), start, limit, accountID)
}
// CreateOrganizationWithContext creates an organization by
// passing the name of the organization.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-post
func (s *OrganizationService) CreateOrganizationWithContext(ctx context.Context, name string) (*Organization, *Response, error) {
apiEndPoint := "rest/servicedeskapi/organization"
organization := OrganizationCreationDTO{
Name: name,
}
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
o := new(Organization)
resp, err := s.client.Do(req, &o)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return o, resp, nil
}
// CreateOrganization wraps CreateOrganizationWithContext using the background context.
func (s *OrganizationService) CreateOrganization(name string) (*Organization, *Response, error) {
return s.CreateOrganizationWithContext(context.Background(), name)
}
// GetOrganizationWithContext returns details of an
// organization. Use this method to get organization
// details whenever your application component is
// passed an organization ID but needs to display
// other organization details.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-get
func (s *OrganizationService) GetOrganizationWithContext(ctx context.Context, organizationID int) (*Organization, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
o := new(Organization)
resp, err := s.client.Do(req, &o)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return o, resp, nil
}
// GetOrganization wraps GetOrganizationWithContext using the background context.
func (s *OrganizationService) GetOrganization(organizationID int) (*Organization, *Response, error) {
return s.GetOrganizationWithContext(context.Background(), organizationID)
}
// DeleteOrganizationWithContext deletes an organization. Note that
// the organization is deleted regardless
// of other associations it may have.
// For example, associations with service desks.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-delete
// Caller must close resp.Body
func (s *OrganizationService) DeleteOrganizationWithContext(ctx context.Context, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// DeleteOrganization wraps DeleteOrganizationWithContext using the background context.
// Caller must close resp.Body
func (s *OrganizationService) DeleteOrganization(organizationID int) (*Response, error) {
return s.DeleteOrganizationWithContext(context.Background(), organizationID)
}
// GetPropertiesKeysWithContext returns the keys of
// all properties for an organization. Use this resource
// when you need to find out what additional properties
// items have been added to an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-get
func (s *OrganizationService) GetPropertiesKeysWithContext(ctx context.Context, organizationID int) (*PropertyKeys, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
pk := new(PropertyKeys)
resp, err := s.client.Do(req, &pk)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return pk, resp, nil
}
// GetPropertiesKeys wraps GetPropertiesKeysWithContext using the background context.
func (s *OrganizationService) GetPropertiesKeys(organizationID int) (*PropertyKeys, *Response, error) {
return s.GetPropertiesKeysWithContext(context.Background(), organizationID)
}
// GetPropertyWithContext returns the value of a property
// from an organization. Use this method to obtain the JSON
// content for an organization's property.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-get
func (s *OrganizationService) GetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*EntityProperty, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
ep := new(EntityProperty)
resp, err := s.client.Do(req, &ep)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return ep, resp, nil
}
// GetProperty wraps GetPropertyWithContext using the background context.
func (s *OrganizationService) GetProperty(organizationID int, propertyKey string) (*EntityProperty, *Response, error) {
return s.GetPropertyWithContext(context.Background(), organizationID, propertyKey)
}
// SetPropertyWithContext sets the value of a
// property for an organization. Use this
// resource to store custom data against an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-put
// Caller must close resp.Body
func (s *OrganizationService) SetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey)
req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// SetProperty wraps SetPropertyWithContext using the background context.
// Caller must close resp.Body
func (s *OrganizationService) SetProperty(organizationID int, propertyKey string) (*Response, error) {
return s.SetPropertyWithContext(context.Background(), organizationID, propertyKey)
}
// DeletePropertyWithContext removes a property from an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-delete
// Caller must close resp.Body
func (s *OrganizationService) DeletePropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// DeleteProperty wraps DeletePropertyWithContext using the background context.
// Caller must close resp.Body
func (s *OrganizationService) DeleteProperty(organizationID int, propertyKey string) (*Response, error) {
return s.DeletePropertyWithContext(context.Background(), organizationID, propertyKey)
}
// GetUsersWithContext returns all the users
// associated with an organization. Use this
// method where you want to provide a list of
// users for an organization or determine if
// a user is associated with an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-get
func (s *OrganizationService) GetUsersWithContext(ctx context.Context, organizationID int, start int, limit int) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user?start=%d&limit=%d", organizationID, start, limit)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
users := new(PagedDTO)
resp, err := s.client.Do(req, &users)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return users, resp, nil
}
// GetUsers wraps GetUsersWithContext using the background context.
func (s *OrganizationService) GetUsers(organizationID int, start int, limit int) (*PagedDTO, *Response, error) {
return s.GetUsersWithContext(context.Background(), organizationID, start, limit)
}
// AddUsersWithContext adds users to an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-post
// Caller must close resp.Body
func (s *OrganizationService) AddUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, users)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// AddUsers wraps AddUsersWithContext using the background context.
// Caller must close resp.Body
func (s *OrganizationService) AddUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) {
return s.AddUsersWithContext(context.Background(), organizationID, users)
}
// RemoveUsersWithContext removes users from an organization.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-delete
// Caller must close resp.Body
func (s *OrganizationService) RemoveUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// RemoveUsers wraps RemoveUsersWithContext using the background context.
// Caller must close resp.Body
func (s *OrganizationService) RemoveUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) {
return s.RemoveUsersWithContext(context.Background(), organizationID, users)
}

View file

@ -1,10 +1,13 @@
package jira
import "fmt"
import (
"context"
"fmt"
)
// PermissionSchemeService handles permissionschemes for the JIRA instance / API.
// PermissionSchemeService handles permissionschemes for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme
type PermissionSchemeService struct {
client *Client
}
@ -25,12 +28,12 @@ type Holder struct {
Expand string `json:"expand" structs:"expand"`
}
// GetList returns a list of all permission schemes
// GetListWithContext returns a list of all permission schemes
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get
func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get
func (s *PermissionSchemeService) GetListWithContext(ctx context.Context) (*PermissionSchemes, *Response, error) {
apiEndpoint := "/rest/api/3/permissionscheme"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -45,12 +48,17 @@ func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, erro
return pss, resp, nil
}
// Get returns a full representation of the permission scheme for the schemeID
// GetList wraps GetListWithContext using the background context.
func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) {
return s.GetListWithContext(context.Background())
}
// GetWithContext returns a full representation of the permission scheme for the schemeID
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get
func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get
func (s *PermissionSchemeService) GetWithContext(ctx context.Context, schemeID int) (*PermissionScheme, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/3/permissionscheme/%d", schemeID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -62,8 +70,13 @@ func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Respons
return nil, resp, jerr
}
if ps.Self == "" {
return nil, resp, fmt.Errorf("No permissionscheme with ID %d found", schemeID)
return nil, resp, fmt.Errorf("no permissionscheme with ID %d found", schemeID)
}
return ps, resp, nil
}
// Get wraps GetWithContext using the background context.
func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) {
return s.GetWithContext(context.Background(), schemeID)
}

View file

@ -1,13 +1,15 @@
package jira
// PriorityService handles priorities for the JIRA instance / API.
import "context"
// PriorityService handles priorities for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority
type PriorityService struct {
client *Client
}
// Priority represents a priority of a JIRA issue.
// Priority represents a priority of a Jira issue.
// Typical types are "Normal", "Moderate", "Urgent", ...
type Priority struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
@ -18,12 +20,12 @@ type Priority struct {
Description string `json:"description,omitempty" structs:"description,omitempty"`
}
// GetList gets all priorities from JIRA
// GetListWithContext gets all priorities from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get
func (s *PriorityService) GetList() ([]Priority, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get
func (s *PriorityService) GetListWithContext(ctx context.Context) ([]Priority, *Response, error) {
apiEndpoint := "rest/api/2/priority"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -35,3 +37,8 @@ func (s *PriorityService) GetList() ([]Priority, *Response, error) {
}
return priorityList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *PriorityService) GetList() ([]Priority, *Response, error) {
return s.GetListWithContext(context.Background())
}

View file

@ -1,14 +1,15 @@
package jira
import (
"context"
"fmt"
"github.com/google/go-querystring/query"
)
// ProjectService handles projects for the JIRA instance / API.
// ProjectService handles projects for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project
type ProjectService struct {
client *Client
}
@ -34,7 +35,7 @@ type ProjectCategory struct {
Description string `json:"description" structs:"description,omitempty"`
}
// Project represents a JIRA Project.
// Project represents a Jira Project.
type Project struct {
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
@ -80,20 +81,25 @@ type PermissionScheme struct {
Permissions []Permission `json:"permissions" structs:"permissions,omitempty"`
}
// GetList gets all projects form JIRA
// GetListWithContext gets all projects form Jira
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
return s.ListWithOptions(&GetQueryOptions{})
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) GetListWithContext(ctx context.Context) (*ProjectList, *Response, error) {
return s.ListWithOptionsWithContext(ctx, &GetQueryOptions{})
}
// ListWithOptions gets all projects form JIRA with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get
// GetList wraps GetListWithContext using the background context.
func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
return s.GetListWithContext(context.Background())
}
// ListWithOptionsWithContext gets all projects form Jira with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get
// a list of all projects and their supported issuetypes
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) ListWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*ProjectList, *Response, error) {
apiEndpoint := "rest/api/2/project"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -116,14 +122,19 @@ func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList
return projectList, resp, nil
}
// Get returns a full representation of the project for the given issue key.
// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
// ListWithOptions wraps ListWithOptionsWithContext using the background context.
func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) {
return s.ListWithOptionsWithContext(context.Background(), options)
}
// GetWithContext returns a full representation of the project for the given issue key.
// Jira will attempt to identify the project by the projectIdOrKey path parameter.
// This can be an project id, or an project key.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) GetWithContext(ctx context.Context, projectID string) (*Project, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -138,14 +149,19 @@ func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
return project, resp, nil
}
// GetPermissionScheme returns a full representation of the permission scheme for the project
// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
// Get wraps GetWithContext using the background context.
func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
return s.GetWithContext(context.Background(), projectID)
}
// GetPermissionSchemeWithContext returns a full representation of the permission scheme for the project
// Jira will attempt to identify the project by the projectIdOrKey path parameter.
// This can be an project id, or an project key.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) GetPermissionSchemeWithContext(ctx context.Context, projectID string) (*PermissionScheme, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -159,3 +175,8 @@ func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionSchem
return ps, resp, nil
}
// GetPermissionScheme wraps GetPermissionSchemeWithContext using the background context.
func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) {
return s.GetPermissionSchemeWithContext(context.Background(), projectID)
}

123
vendor/github.com/andygrunwald/go-jira/request.go generated vendored Normal file
View file

@ -0,0 +1,123 @@
package jira
import (
"context"
"fmt"
)
// RequestService handles ServiceDesk customer requests for the Jira instance / API.
type RequestService struct {
client *Client
}
// Request represents a ServiceDesk customer request.
type Request struct {
IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"`
IssueKey string `json:"issueKey,omitempty" structs:"issueKey,omitempty"`
TypeID string `json:"requestTypeId,omitempty" structs:"requestTypeId,omitempty"`
ServiceDeskID string `json:"serviceDeskId,omitempty" structs:"serviceDeskId,omitempty"`
Reporter *Customer `json:"reporter,omitempty" structs:"reporter,omitempty"`
FieldValues []RequestFieldValue `json:"requestFieldValues,omitempty" structs:"requestFieldValues,omitempty"`
Status *RequestStatus `json:"currentStatus,omitempty" structs:"currentStatus,omitempty"`
Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"`
Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"`
}
// RequestFieldValue is a request field.
type RequestFieldValue struct {
FieldID string `json:"fieldId,omitempty" structs:"fieldId,omitempty"`
Label string `json:"label,omitempty" structs:"label,omitempty"`
Value string `json:"value,omitempty" structs:"value,omitempty"`
}
// RequestDate is the date format used in requests.
type RequestDate struct {
ISO8601 string `json:"iso8601,omitempty" structs:"iso8601,omitempty"`
Jira string `json:"jira,omitempty" structs:"jira,omitempty"`
Friendly string `json:"friendly,omitempty" structs:"friendly,omitempty"`
Epoch int64 `json:"epoch,omitempty" structs:"epoch,omitempty"`
}
// RequestStatus is the status for a request.
type RequestStatus struct {
Status string
Category string
Date RequestDate
}
// RequestComment is a comment for a request.
type RequestComment struct {
ID string `json:"id,omitempty" structs:"id,omitempty"`
Body string `json:"body,omitempty" structs:"body,omitempty"`
Public bool `json:"public" structs:"public"`
Author *Customer `json:"author,omitempty" structs:"author,omitempty"`
Created *RequestDate `json:"created,omitempty" structs:"created,omitempty"`
Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"`
Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"`
}
// CreateWithContext creates a new request.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-post
func (r *RequestService) CreateWithContext(ctx context.Context, requester string, participants []string, request *Request) (*Request, *Response, error) {
apiEndpoint := "rest/servicedeskapi/request"
payload := struct {
*Request
FieldValues map[string]string `json:"requestFieldValues,omitempty"`
Requester string `json:"raiseOnBehalfOf,omitempty"`
Participants []string `json:"requestParticipants,omitempty"`
}{
Request: request,
FieldValues: make(map[string]string),
Requester: requester,
Participants: participants,
}
for _, field := range request.FieldValues {
payload.FieldValues[field.FieldID] = field.Value
}
req, err := r.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload)
if err != nil {
return nil, nil, err
}
responseRequest := new(Request)
resp, err := r.client.Do(req, responseRequest)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}
return responseRequest, resp, nil
}
// Create wraps CreateWithContext using the background context.
func (r *RequestService) Create(requester string, participants []string, request *Request) (*Request, *Response, error) {
return r.CreateWithContext(context.Background(), requester, participants, request)
}
// CreateCommentWithContext creates a comment on a request.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-request/#api-rest-servicedeskapi-request-issueidorkey-comment-post
func (r *RequestService) CreateCommentWithContext(ctx context.Context, issueIDOrKey string, comment *RequestComment) (*RequestComment, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/servicedeskapi/request/%v/comment", issueIDOrKey)
req, err := r.client.NewRequestWithContext(ctx, "POST", apiEndpoint, comment)
if err != nil {
return nil, nil, err
}
responseComment := new(RequestComment)
resp, err := r.client.Do(req, responseComment)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}
return responseComment, resp, nil
}
// CreateComment wraps CreateCommentWithContext using the background context.
func (r *RequestService) CreateComment(issueIDOrKey string, comment *RequestComment) (*RequestComment, *Response, error) {
return r.CreateCommentWithContext(context.Background(), issueIDOrKey, comment)
}

View file

@ -0,0 +1,24 @@
//go:build go1.13
// +build go1.13
// This file provides glue to use Context in `http.Request` with
// Go version 1.13 and higher.
// The function `http.NewRequestWithContext` has been added in Go 1.13.
// Before the release 1.13, to use Context we need creat `http.Request`
// then use the method `WithContext` to create a new `http.Request`
// with Context from the existing `http.Request`.
//
// Doc: https://golang.org/doc/go1.13#net/http
package jira
import (
"context"
"io"
"net/http"
)
func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
return http.NewRequestWithContext(ctx, method, url, body)
}

View file

@ -0,0 +1,29 @@
//go:build !go1.13
// +build !go1.13
// This file provides glue to use Context in `http.Request` with
// Go version before 1.13.
// The function `http.NewRequestWithContext` has been added in Go 1.13.
// Before the release 1.13, to use Context we need creat `http.Request`
// then use the method `WithContext` to create a new `http.Request`
// with Context from the existing `http.Request`.
//
// Doc: https://golang.org/doc/go1.13#net/http
package jira
import (
"context"
"io"
"net/http"
)
func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
r, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
return r.WithContext(ctx), nil
}

View file

@ -1,13 +1,15 @@
package jira
// ResolutionService handles resolutions for the JIRA instance / API.
import "context"
// ResolutionService handles resolutions for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution
type ResolutionService struct {
client *Client
}
// Resolution represents a resolution of a JIRA issue.
// Resolution represents a resolution of a Jira issue.
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
type Resolution struct {
Self string `json:"self" structs:"self"`
@ -16,12 +18,12 @@ type Resolution struct {
Name string `json:"name" structs:"name"`
}
// GetList gets all resolutions from JIRA
// GetListWithContext gets all resolutions from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get
func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get
func (s *ResolutionService) GetListWithContext(ctx context.Context) ([]Resolution, *Response, error) {
apiEndpoint := "rest/api/2/resolution"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -33,3 +35,8 @@ func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
}
return resolutionList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
return s.GetListWithContext(context.Background())
}

View file

@ -1,17 +1,18 @@
package jira
import (
"context"
"fmt"
)
// RoleService handles roles for the JIRA instance / API.
// RoleService handles roles for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role
type RoleService struct {
client *Client
}
// Role represents a JIRA product role
// Role represents a Jira product role
type Role struct {
Self string `json:"self" structs:"self"`
Name string `json:"name" structs:"name"`
@ -20,7 +21,7 @@ type Role struct {
Actors []*Actor `json:"actors" structs:"actors"`
}
// Actor represents a JIRA actor
// Actor represents a Jira actor
type Actor struct {
ID int `json:"id" structs:"id"`
DisplayName string `json:"displayName" structs:"displayName"`
@ -35,12 +36,12 @@ type ActorUser struct {
AccountID string `json:"accountId" structs:"accountId"`
}
// GetList returns a list of all available project roles
// GetListWithContext returns a list of all available project roles
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get
func (s *RoleService) GetList() (*[]Role, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get
func (s *RoleService) GetListWithContext(ctx context.Context) (*[]Role, *Response, error) {
apiEndpoint := "rest/api/3/role"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -53,12 +54,17 @@ func (s *RoleService) GetList() (*[]Role, *Response, error) {
return roles, resp, err
}
// Get retreives a single Role from Jira
// GetList wraps GetListWithContext using the background context.
func (s *RoleService) GetList() (*[]Role, *Response, error) {
return s.GetListWithContext(context.Background())
}
// GetWithContext retreives a single Role from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get
func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get
func (s *RoleService) GetWithContext(ctx context.Context, roleID int) (*Role, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/3/role/%d", roleID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -69,8 +75,13 @@ func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
return nil, resp, jerr
}
if role.Self == "" {
return nil, resp, fmt.Errorf("No role with ID %d found", roleID)
return nil, resp, fmt.Errorf("no role with ID %d found", roleID)
}
return role, resp, err
}
// Get wraps GetWithContext using the background context.
func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
return s.GetWithContext(context.Background(), roleID)
}

227
vendor/github.com/andygrunwald/go-jira/servicedesk.go generated vendored Normal file
View file

@ -0,0 +1,227 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"github.com/google/go-querystring/query"
)
// ServiceDeskService handles ServiceDesk for the Jira instance / API.
type ServiceDeskService struct {
client *Client
}
// ServiceDeskOrganizationDTO is a DTO for ServiceDesk organizations
type ServiceDeskOrganizationDTO struct {
OrganizationID int `json:"organizationId,omitempty" structs:"organizationId,omitempty"`
}
// GetOrganizationsWithContext returns a list of
// all organizations associated with a service desk.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-get
func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, serviceDeskID interface{}, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization?start=%d&limit=%d", serviceDeskID, start, limit)
if accountID != "" {
apiEndPoint += fmt.Sprintf("&accountId=%s", accountID)
}
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil)
req.Header.Set("Accept", "application/json")
if err != nil {
return nil, nil, err
}
orgs := new(PagedDTO)
resp, err := s.client.Do(req, &orgs)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}
return orgs, resp, nil
}
// GetOrganizations wraps GetOrganizationsWithContext using the background context.
func (s *ServiceDeskService) GetOrganizations(serviceDeskID interface{}, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
return s.GetOrganizationsWithContext(context.Background(), serviceDeskID, start, limit, accountID)
}
// AddOrganizationWithContext adds an organization to
// a service desk. If the organization ID is already
// associated with the service desk, no change is made
// and the resource returns a 204 success code.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-post
// Caller must close resp.Body
func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, serviceDeskID interface{}, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization", serviceDeskID)
organization := ServiceDeskOrganizationDTO{
OrganizationID: organizationID,
}
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// AddOrganization wraps AddOrganizationWithContext using the background context.
// Caller must close resp.Body
func (s *ServiceDeskService) AddOrganization(serviceDeskID interface{}, organizationID int) (*Response, error) {
return s.AddOrganizationWithContext(context.Background(), serviceDeskID, organizationID)
}
// RemoveOrganizationWithContext removes an organization
// from a service desk. If the organization ID does not
// match an organization associated with the service desk,
// no change is made and the resource returns a 204 success code.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-delete
// Caller must close resp.Body
func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context, serviceDeskID interface{}, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization", serviceDeskID)
organization := ServiceDeskOrganizationDTO{
OrganizationID: organizationID,
}
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, organization)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}
return resp, nil
}
// RemoveOrganization wraps RemoveOrganizationWithContext using the background context.
// Caller must close resp.Body
func (s *ServiceDeskService) RemoveOrganization(serviceDeskID interface{}, organizationID int) (*Response, error) {
return s.RemoveOrganizationWithContext(context.Background(), serviceDeskID, organizationID)
}
// AddCustomersWithContext adds customers to the given service desk.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-post
func (s *ServiceDeskService) AddCustomersWithContext(ctx context.Context, serviceDeskID interface{}, acountIDs ...string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/customer", serviceDeskID)
payload := struct {
AccountIDs []string `json:"accountIds"`
}{
AccountIDs: acountIDs,
}
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
return resp, NewJiraError(resp, err)
}
defer resp.Body.Close()
_, _ = io.Copy(ioutil.Discard, resp.Body)
return resp, nil
}
// AddCustomers wraps AddCustomersWithContext using the background context.
func (s *ServiceDeskService) AddCustomers(serviceDeskID interface{}, acountIDs ...string) (*Response, error) {
return s.AddCustomersWithContext(context.Background(), serviceDeskID, acountIDs...)
}
// RemoveCustomersWithContext removes customers to the given service desk.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-delete
func (s *ServiceDeskService) RemoveCustomersWithContext(ctx context.Context, serviceDeskID interface{}, acountIDs ...string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/customer", serviceDeskID)
payload := struct {
AccountIDs []string `json:"accountIDs"`
}{
AccountIDs: acountIDs,
}
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, payload)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
return resp, NewJiraError(resp, err)
}
defer resp.Body.Close()
_, _ = io.Copy(ioutil.Discard, resp.Body)
return resp, nil
}
// RemoveCustomers wraps RemoveCustomersWithContext using the background context.
func (s *ServiceDeskService) RemoveCustomers(serviceDeskID interface{}, acountIDs ...string) (*Response, error) {
return s.RemoveCustomersWithContext(context.Background(), serviceDeskID, acountIDs...)
}
// ListCustomersWithContext lists customers for a ServiceDesk.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-customer-get
func (s *ServiceDeskService) ListCustomersWithContext(ctx context.Context, serviceDeskID interface{}, options *CustomerListOptions) (*CustomerList, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/customer", serviceDeskID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
// this is an experiemntal endpoint
req.Header.Set("X-ExperimentalApi", "opt-in")
if options != nil {
q, err := query.Values(options)
if err != nil {
return nil, nil, err
}
req.URL.RawQuery = q.Encode()
}
resp, err := s.client.Do(req, nil)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}
defer resp.Body.Close()
customerList := new(CustomerList)
if err := json.NewDecoder(resp.Body).Decode(customerList); err != nil {
return nil, resp, fmt.Errorf("could not unmarshall the data into struct")
}
return customerList, resp, nil
}
// ListCustomers wraps ListCustomersWithContext using the background context.
func (s *ServiceDeskService) ListCustomers(serviceDeskID interface{}, options *CustomerListOptions) (*CustomerList, *Response, error) {
return s.ListCustomersWithContext(context.Background(), serviceDeskID, options)
}

View file

@ -1,12 +1,13 @@
package jira
import (
"context"
"fmt"
"github.com/google/go-querystring/query"
)
// SprintService handles sprints in JIRA Agile API.
// SprintService handles sprints in Jira Agile API.
// See https://docs.atlassian.com/jira-software/REST/cloud/
type SprintService struct {
client *Client
@ -22,17 +23,18 @@ type IssuesInSprintResult struct {
Issues []Issue `json:"issues"`
}
// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id.
// MoveIssuesToSprintWithContext moves issues to a sprint, for a given sprint Id.
// Issues can only be moved to open or active sprints.
// The maximum number of issues that can be moved in one operation is 50.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint
func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) {
// Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint
// Caller must close resp.Body
func (s *SprintService) MoveIssuesToSprintWithContext(ctx context.Context, sprintID int, issueIDs []string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
payload := IssuesWrapper{Issues: issueIDs}
req, err := s.client.NewRequest("POST", apiEndpoint, payload)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload)
if err != nil {
return nil, err
@ -45,15 +47,21 @@ func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Re
return resp, err
}
// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id.
// MoveIssuesToSprint wraps MoveIssuesToSprintWithContext using the background context.
// Caller must close resp.Body
func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) {
return s.MoveIssuesToSprintWithContext(context.Background(), sprintID, issueIDs)
}
// GetIssuesForSprintWithContext returns all issues in a sprint, for a given sprint Id.
// This only includes issues that the user has permission to view.
// By default, the returned issues are ordered by rank.
//
// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) {
// Jira API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
func (s *SprintService) GetIssuesForSprintWithContext(ctx context.Context, sprintID int) ([]Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -68,20 +76,25 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er
return result.Issues, resp, err
}
// GetIssue returns a full representation of the issue for the given issue key.
// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
// GetIssuesForSprint wraps GetIssuesForSprintWithContext using the background context.
func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) {
return s.GetIssuesForSprintWithContext(context.Background(), sprintID)
}
// GetIssueWithContext returns a full representation of the issue for the given issue key.
// Jira will attempt to identify the issue by the issueIdOrKey path parameter.
// This can be an issue id, or an issue key.
// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
// If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
//
// The given options will be appended to the query string
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue
// Jira API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue
//
// TODO: create agile service for holding all agile apis' implementation
func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
func (s *SprintService) GetIssueWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -105,3 +118,8 @@ func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Iss
return issue, resp, nil
}
// GetIssue wraps GetIssueWithContext using the background context.
func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
return s.GetIssueWithContext(context.Background(), issueID, options)
}

View file

@ -1,15 +1,17 @@
package jira
// StatusService handles staties for the JIRA instance / API.
import "context"
// StatusService handles staties for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses
type StatusService struct {
client *Client
}
// Status represents the current status of a JIRA issue.
// Status represents the current status of a Jira issue.
// Typical status are "Open", "In Progress", "Closed", ...
// Status can be user defined in every JIRA instance.
// Status can be user defined in every Jira instance.
type Status struct {
Self string `json:"self" structs:"self"`
Description string `json:"description" structs:"description"`
@ -19,12 +21,12 @@ type Status struct {
StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"`
}
// GetAllStatuses returns a list of all statuses associated with workflows.
// GetAllStatusesWithContext returns a list of all statuses associated with workflows.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get
func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get
func (s *StatusService) GetAllStatusesWithContext(ctx context.Context) ([]Status, *Response, error) {
apiEndpoint := "rest/api/2/status"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
@ -38,3 +40,8 @@ func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) {
return statusList, resp, nil
}
// GetAllStatuses wraps GetAllStatusesWithContext using the background context.
func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) {
return s.GetAllStatusesWithContext(context.Background())
}

View file

@ -1,14 +1,16 @@
package jira
// StatusCategoryService handles status categories for the JIRA instance / API.
import "context"
// StatusCategoryService handles status categories for the Jira instance / API.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory
type StatusCategoryService struct {
client *Client
}
// StatusCategory represents the category a status belongs to.
// Those categories can be user defined in every JIRA instance.
// Those categories can be user defined in every Jira instance.
type StatusCategory struct {
Self string `json:"self" structs:"self"`
ID int `json:"id" structs:"id"`
@ -17,7 +19,7 @@ type StatusCategory struct {
ColorName string `json:"colorName" structs:"colorName"`
}
// These constants are the keys of the default JIRA status categories
// These constants are the keys of the default Jira status categories
const (
StatusCategoryComplete = "done"
StatusCategoryInProgress = "indeterminate"
@ -25,12 +27,12 @@ const (
StatusCategoryUndefined = "undefined"
)
// GetList gets all status categories from JIRA
// GetListWithContext gets all status categories from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get
func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get
func (s *StatusCategoryService) GetListWithContext(ctx context.Context) ([]StatusCategory, *Response, error) {
apiEndpoint := "rest/api/2/statuscategory"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -42,3 +44,8 @@ func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
}
return statusCategoryList, resp, nil
}
// GetList wraps GetListWithContext using the background context.
func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
return s.GetListWithContext(context.Background())
}

9
vendor/github.com/andygrunwald/go-jira/types.go generated vendored Normal file
View file

@ -0,0 +1,9 @@
package jira
// Bool is a helper routine that allocates a new bool value
// to store v and returns a pointer to it.
func Bool(v bool) *bool {
p := new(bool)
*p = v
return p
}

View file

@ -1,25 +1,24 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
)
// UserService handles users for the JIRA instance / API.
// UserService handles users for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Users
type UserService struct {
client *Client
}
// User represents a JIRA user.
// User represents a Jira user.
type User struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"`
AccountType string `json:"accountType,omitempty" structs:"accountType,omitempty"`
// TODO: name & key are deprecated, see:
// https://developer.atlassian.com/cloud/jira/platform/api-changes-for-user-privacy-announcement/
Self string `json:"self,omitempty" structs:"self,omitempty"`
AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"`
AccountType string `json:"accountType,omitempty" structs:"accountType,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
Password string `json:"-"`
@ -47,12 +46,12 @@ type userSearch []userSearchParam
type userSearchF func(userSearch) userSearch
// Get gets user info from JIRA
// GetWithContext gets user info from Jira using its Account Id
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser
func (s *UserService) Get(username string) (*User, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-get
func (s *UserService) GetWithContext(ctx context.Context, accountId string) (*User, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -65,12 +64,41 @@ func (s *UserService) Get(username string) (*User, *Response, error) {
return user, resp, nil
}
// Create creates an user in JIRA.
// Get wraps GetWithContext using the background context.
func (s *UserService) Get(accountId string) (*User, *Response, error) {
return s.GetWithContext(context.Background(), accountId)
}
// GetByAccountIDWithContext gets user info from Jira
// Searching by another parameter that is not accountId is deprecated,
// but this method is kept for backwards compatibility
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser
func (s *UserService) GetByAccountIDWithContext(ctx context.Context, accountID string) (*User, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountID)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
user := new(User)
resp, err := s.client.Do(req, user)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}
return user, resp, nil
}
// GetByAccountID wraps GetByAccountIDWithContext using the background context.
func (s *UserService) GetByAccountID(accountID string) (*User, *Response, error) {
return s.GetByAccountIDWithContext(context.Background(), accountID)
}
// CreateWithContext creates an user in Jira.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser
func (s *UserService) Create(user *User) (*User, *Response, error) {
// Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser
func (s *UserService) CreateWithContext(ctx context.Context, user *User) (*User, *Response, error) {
apiEndpoint := "/rest/api/2/user"
req, err := s.client.NewRequest("POST", apiEndpoint, user)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, user)
if err != nil {
return nil, nil, err
}
@ -84,24 +112,30 @@ func (s *UserService) Create(user *User) (*User, *Response, error) {
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
e := fmt.Errorf("Could not read the returned data")
e := fmt.Errorf("could not read the returned data")
return nil, resp, NewJiraError(resp, e)
}
err = json.Unmarshal(data, responseUser)
if err != nil {
e := fmt.Errorf("Could not unmarshall the data into struct")
e := fmt.Errorf("could not unmarshall the data into struct")
return nil, resp, NewJiraError(resp, e)
}
return responseUser, resp, nil
}
// Delete deletes an user from JIRA.
// Create wraps CreateWithContext using the background context.
func (s *UserService) Create(user *User) (*User, *Response, error) {
return s.CreateWithContext(context.Background(), user)
}
// DeleteWithContext deletes an user from Jira.
// Returns http.StatusNoContent on success.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-user-delete
func (s *UserService) Delete(username string) (*Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-delete
// Caller must close resp.Body
func (s *UserService) DeleteWithContext(ctx context.Context, accountId string) (*Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId)
req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil)
if err != nil {
return nil, err
}
@ -113,12 +147,18 @@ func (s *UserService) Delete(username string) (*Response, error) {
return resp, nil
}
// GetGroups returns the groups which the user belongs to
// Delete wraps DeleteWithContext using the background context.
// Caller must close resp.Body
func (s *UserService) Delete(accountId string) (*Response, error) {
return s.DeleteWithContext(context.Background(), accountId)
}
// GetGroupsWithContext returns the groups which the user belongs to
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups
func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-groups-get
func (s *UserService) GetGroupsWithContext(ctx context.Context, accountId string) (*[]UserGroup, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?accountId=%s", accountId)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -131,12 +171,17 @@ func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error
return userGroups, resp, nil
}
// Get information about the current logged-in user
// GetGroups wraps GetGroupsWithContext using the background context.
func (s *UserService) GetGroups(accountId string) (*[]UserGroup, *Response, error) {
return s.GetGroupsWithContext(context.Background(), accountId)
}
// GetSelfWithContext information about the current logged-in user
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get
func (s *UserService) GetSelf() (*User, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-myself-get
func (s *UserService) GetSelfWithContext(ctx context.Context) (*User, *Response, error) {
const apiEndpoint = "rest/api/2/myself"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -148,6 +193,11 @@ func (s *UserService) GetSelf() (*User, *Response, error) {
return &user, resp, nil
}
// GetSelf wraps GetSelfWithContext using the background context.
func (s *UserService) GetSelf() (*User, *Response, error) {
return s.GetSelfWithContext(context.Background())
}
// WithMaxResults sets the max results to return
func WithMaxResults(maxResults int) userSearchF {
return func(s userSearch) userSearch {
@ -180,14 +230,38 @@ func WithInactive(inactive bool) userSearchF {
}
}
// Find searches for user info from JIRA:
// It can find users by email, username or name
// WithUsername sets the username to search
func WithUsername(username string) userSearchF {
return func(s userSearch) userSearch {
s = append(s, userSearchParam{name: "username", value: username})
return s
}
}
// WithAccountId sets the account id to search
func WithAccountId(accountId string) userSearchF {
return func(s userSearch) userSearch {
s = append(s, userSearchParam{name: "accountId", value: accountId})
return s
}
}
// WithProperty sets the property (Property keys are specified by path) to search
func WithProperty(property string) userSearchF {
return func(s userSearch) userSearch {
s = append(s, userSearchParam{name: "property", value: property})
return s
}
}
// FindWithContext searches for user info from Jira:
// It can find users by email or display name using the query parameter
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers
func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-user-search-get
func (s *UserService) FindWithContext(ctx context.Context, property string, tweaks ...userSearchF) ([]User, *Response, error) {
search := []userSearchParam{
{
name: "username",
name: "query",
value: property,
},
}
@ -201,7 +275,7 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res
}
apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString[:len(queryString)-1])
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -213,3 +287,8 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res
}
return users, resp, nil
}
// Find wraps FindWithContext using the background context.
func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) {
return s.FindWithContext(context.Background(), property, tweaks...)
}

View file

@ -1,14 +1,15 @@
package jira
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
)
// VersionService handles Versions for the JIRA instance / API.
// VersionService handles Versions for the Jira instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version
// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version
type VersionService struct {
client *Client
}
@ -19,20 +20,20 @@ type Version struct {
ID string `json:"id,omitempty" structs:"id,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Description string `json:"description,omitempty" structs:"description,omitempty"`
Archived bool `json:"archived,omitempty" structs:"archived,omitempty"`
Released bool `json:"released,omitempty" structs:"released,omitempty"`
Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"`
Released *bool `json:"released,omitempty" structs:"released,omitempty"`
ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"`
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number
StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"`
}
// Get gets version info from JIRA
// GetWithContext gets version info from Jira
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get
func (s *VersionService) Get(versionID int) (*Version, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get
func (s *VersionService) GetWithContext(ctx context.Context, versionID int) (*Version, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/version/%v", versionID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
@ -45,12 +46,17 @@ func (s *VersionService) Get(versionID int) (*Version, *Response, error) {
return version, resp, nil
}
// Create creates a version in JIRA.
// Get wraps GetWithContext using the background context.
func (s *VersionService) Get(versionID int) (*Version, *Response, error) {
return s.GetWithContext(context.Background(), versionID)
}
// CreateWithContext creates a version in Jira.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post
func (s *VersionService) Create(version *Version) (*Version, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post
func (s *VersionService) CreateWithContext(ctx context.Context, version *Version) (*Version, *Response, error) {
apiEndpoint := "/rest/api/2/version"
req, err := s.client.NewRequest("POST", apiEndpoint, version)
req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, version)
if err != nil {
return nil, nil, err
}
@ -64,23 +70,29 @@ func (s *VersionService) Create(version *Version) (*Version, *Response, error) {
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
e := fmt.Errorf("Could not read the returned data")
e := fmt.Errorf("could not read the returned data")
return nil, resp, NewJiraError(resp, e)
}
err = json.Unmarshal(data, responseVersion)
if err != nil {
e := fmt.Errorf("Could not unmarshall the data into struct")
e := fmt.Errorf("could not unmarshall the data into struct")
return nil, resp, NewJiraError(resp, e)
}
return responseVersion, resp, nil
}
// Update updates a version from a JSON representation.
// Create wraps CreateWithContext using the background context.
func (s *VersionService) Create(version *Version) (*Version, *Response, error) {
return s.CreateWithContext(context.Background(), version)
}
// UpdateWithContext updates a version from a JSON representation.
//
// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put
func (s *VersionService) Update(version *Version) (*Version, *Response, error) {
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put
// Caller must close resp.Body
func (s *VersionService) UpdateWithContext(ctx context.Context, version *Version) (*Version, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/version/%v", version.ID)
req, err := s.client.NewRequest("PUT", apiEndpoint, version)
req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, version)
if err != nil {
return nil, nil, err
}
@ -95,3 +107,9 @@ func (s *VersionService) Update(version *Version) (*Version, *Response, error) {
ret := *version
return &ret, resp, nil
}
// Update wraps UpdateWithContext using the background context.
// Caller must close resp.Body
func (s *VersionService) Update(version *Version) (*Version, *Response, error) {
return s.UpdateWithContext(context.Background(), version)
}

View file

@ -1,30 +0,0 @@
package mssql
import (
"log"
)
type Logger interface {
Printf(format string, v ...interface{})
Println(v ...interface{})
}
type optionalLogger struct {
logger Logger
}
func (o optionalLogger) Printf(format string, v ...interface{}) {
if o.logger != nil {
o.logger.Printf(format, v...)
} else {
log.Printf(format, v...)
}
}
func (o optionalLogger) Println(v ...interface{}) {
if o.logger != nil {
o.logger.Println(v...)
} else {
log.Println(v...)
}
}

View file

@ -1,6 +1,8 @@
language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
- tip
sudo: false
before_install:

View file

@ -81,8 +81,8 @@ n := s.Names() // Get a []string
f := s.Field(name) // Get a *Field based on the given field name
f, ok := s.FieldOk(name) // Get a *Field based on the given field name
n := s.Name() // Get the struct name
h := s.HasZero() // Check if any field is initialized
z := s.IsZero() // Check if all fields are initialized
h := s.HasZero() // Check if any field is uninitialized
z := s.IsZero() // Check if all fields are uninitialized
```
### Field methods

View file

@ -203,9 +203,7 @@ func (s *Struct) Values() []interface{} {
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
// look out for embedded structs, and convert them to a
// []interface{} to be added to the final values slice
for _, embeddedVal := range Values(val.Interface()) {
t = append(t, embeddedVal)
}
t = append(t, Values(val.Interface())...)
} else {
t = append(t, val.Interface())
}
@ -573,7 +571,7 @@ func (s *Struct) nested(val reflect.Value) interface{} {
break
}
slices := make([]interface{}, val.Len(), val.Len())
slices := make([]interface{}, val.Len())
for x := 0; x < val.Len(); x++ {
slices[x] = s.nested(val.Index(x))
}

View file

@ -5,7 +5,7 @@ import "strings"
// tagOptions contains a slice of tag options
type tagOptions []string
// Has returns true if the given optiton is available in tagOptions
// Has returns true if the given option is available in tagOptions
func (t tagOptions) Has(opt string) bool {
for _, tagOpt := range t {
if tagOpt == opt {

0
vendor/github.com/felixge/httpsnoop/.gitignore generated vendored Normal file
View file

6
vendor/github.com/felixge/httpsnoop/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,6 @@
language: go
go:
- 1.6
- 1.7
- 1.8

19
vendor/github.com/felixge/httpsnoop/LICENSE.txt generated vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com)
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.

10
vendor/github.com/felixge/httpsnoop/Makefile generated vendored Normal file
View file

@ -0,0 +1,10 @@
.PHONY: ci generate clean
ci: clean generate
go test -v ./...
generate:
go generate .
clean:
rm -rf *_generated*.go

95
vendor/github.com/felixge/httpsnoop/README.md generated vendored Normal file
View file

@ -0,0 +1,95 @@
# httpsnoop
Package httpsnoop provides an easy way to capture http related metrics (i.e.
response time, bytes written, and http status code) from your application's
http.Handlers.
Doing this requires non-trivial wrapping of the http.ResponseWriter interface,
which is also exposed for users interested in a more low-level API.
[![GoDoc](https://godoc.org/github.com/felixge/httpsnoop?status.svg)](https://godoc.org/github.com/felixge/httpsnoop)
[![Build Status](https://travis-ci.org/felixge/httpsnoop.svg?branch=master)](https://travis-ci.org/felixge/httpsnoop)
## Usage Example
```go
// myH is your app's http handler, perhaps a http.ServeMux or similar.
var myH http.Handler
// wrappedH wraps myH in order to log every request.
wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m := httpsnoop.CaptureMetrics(myH, w, r)
log.Printf(
"%s %s (code=%d dt=%s written=%d)",
r.Method,
r.URL,
m.Code,
m.Duration,
m.Written,
)
})
http.ListenAndServe(":8080", wrappedH)
```
## Why this package exists
Instrumenting an application's http.Handler is surprisingly difficult.
However if you google for e.g. "capture ResponseWriter status code" you'll find
lots of advise and code examples that suggest it to be a fairly trivial
undertaking. Unfortunately everything I've seen so far has a high chance of
breaking your application.
The main problem is that a `http.ResponseWriter` often implements additional
interfaces such as `http.Flusher`, `http.CloseNotifier`, `http.Hijacker`, `http.Pusher`, and
`io.ReaderFrom`. So the naive approach of just wrapping `http.ResponseWriter`
in your own struct that also implements the `http.ResponseWriter` interface
will hide the additional interfaces mentioned above. This has a high change of
introducing subtle bugs into any non-trivial application.
Another approach I've seen people take is to return a struct that implements
all of the interfaces above. However, that's also problematic, because it's
difficult to fake some of these interfaces behaviors when the underlying
`http.ResponseWriter` doesn't have an implementation. It's also dangerous,
because an application may choose to operate differently, merely because it
detects the presence of these additional interfaces.
This package solves this problem by checking which additional interfaces a
`http.ResponseWriter` implements, returning a wrapped version implementing the
exact same set of interfaces.
Additionally this package properly handles edge cases such as `WriteHeader` not
being called, or called more than once, as well as concurrent calls to
`http.ResponseWriter` methods, and even calls happening after the wrapped
`ServeHTTP` has already returned.
Unfortunately this package is not perfect either. It's possible that it is
still missing some interfaces provided by the go core (let me know if you find
one), and it won't work for applications adding their own interfaces into the
mix. You can however use `httpsnoop.Unwrap(w)` to access the underlying
`http.ResponseWriter` and type-assert the result to its other interfaces.
However, hopefully the explanation above has sufficiently scared you of rolling
your own solution to this problem. httpsnoop may still break your application,
but at least it tries to avoid it as much as possible.
Anyway, the real problem here is that smuggling additional interfaces inside
`http.ResponseWriter` is a problematic design choice, but it probably goes as
deep as the Go language specification itself. But that's okay, I still prefer
Go over the alternatives ;).
## Performance
```
BenchmarkBaseline-8 20000 94912 ns/op
BenchmarkCaptureMetrics-8 20000 95461 ns/op
```
As you can see, using `CaptureMetrics` on a vanilla http.Handler introduces an
overhead of ~500 ns per http request on my machine. However, the margin of
error appears to be larger than that, therefor it should be reasonable to
assume that the overhead introduced by `CaptureMetrics` is absolutely
negligible.
## License
MIT

86
vendor/github.com/felixge/httpsnoop/capture_metrics.go generated vendored Normal file
View file

@ -0,0 +1,86 @@
package httpsnoop
import (
"io"
"net/http"
"time"
)
// Metrics holds metrics captured from CaptureMetrics.
type Metrics struct {
// Code is the first http response code passed to the WriteHeader func of
// the ResponseWriter. If no such call is made, a default code of 200 is
// assumed instead.
Code int
// Duration is the time it took to execute the handler.
Duration time.Duration
// Written is the number of bytes successfully written by the Write or
// ReadFrom function of the ResponseWriter. ResponseWriters may also write
// data to their underlaying connection directly (e.g. headers), but those
// are not tracked. Therefor the number of Written bytes will usually match
// the size of the response body.
Written int64
}
// CaptureMetrics wraps the given hnd, executes it with the given w and r, and
// returns the metrics it captured from it.
func CaptureMetrics(hnd http.Handler, w http.ResponseWriter, r *http.Request) Metrics {
return CaptureMetricsFn(w, func(ww http.ResponseWriter) {
hnd.ServeHTTP(ww, r)
})
}
// CaptureMetricsFn wraps w and calls fn with the wrapped w and returns the
// resulting metrics. This is very similar to CaptureMetrics (which is just
// sugar on top of this func), but is a more usable interface if your
// application doesn't use the Go http.Handler interface.
func CaptureMetricsFn(w http.ResponseWriter, fn func(http.ResponseWriter)) Metrics {
m := Metrics{Code: http.StatusOK}
m.CaptureMetrics(w, fn)
return m
}
// CaptureMetrics wraps w and calls fn with the wrapped w and updates
// Metrics m with the resulting metrics. This is similar to CaptureMetricsFn,
// but allows one to customize starting Metrics object.
func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWriter)) {
var (
start = time.Now()
headerWritten bool
hooks = Hooks{
WriteHeader: func(next WriteHeaderFunc) WriteHeaderFunc {
return func(code int) {
next(code)
if !headerWritten {
m.Code = code
headerWritten = true
}
}
},
Write: func(next WriteFunc) WriteFunc {
return func(p []byte) (int, error) {
n, err := next(p)
m.Written += int64(n)
headerWritten = true
return n, err
}
},
ReadFrom: func(next ReadFromFunc) ReadFromFunc {
return func(src io.Reader) (int64, error) {
n, err := next(src)
headerWritten = true
m.Written += n
return n, err
}
},
}
)
fn(Wrap(w, hooks))
m.Duration += time.Since(start)
}

10
vendor/github.com/felixge/httpsnoop/docs.go generated vendored Normal file
View file

@ -0,0 +1,10 @@
// Package httpsnoop provides an easy way to capture http related metrics (i.e.
// response time, bytes written, and http status code) from your application's
// http.Handlers.
//
// Doing this requires non-trivial wrapping of the http.ResponseWriter
// interface, which is also exposed for users interested in a more low-level
// API.
package httpsnoop
//go:generate go run codegen/main.go

View file

@ -0,0 +1,436 @@
// +build go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// PushFunc is part of the http.Pusher interface.
type PushFunc func(target string, opts *http.PushOptions) error
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
Push func(PushFunc) PushFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
// - http.Pusher
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
_, i4 := w.(http.Pusher)
switch {
// combination 1/32
case !i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/32
case !i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Pusher
}{rw, rw, rw}
// combination 3/32
case !i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 4/32
case !i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw}
// combination 5/32
case !i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 6/32
case !i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
http.Pusher
}{rw, rw, rw, rw}
// combination 7/32
case !i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 8/32
case !i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 9/32
case !i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 10/32
case !i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw}
// combination 11/32
case !i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 12/32
case !i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 13/32
case !i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 14/32
case !i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 15/32
case !i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 16/32
case !i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 17/32
case i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 18/32
case i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Pusher
}{rw, rw, rw, rw}
// combination 19/32
case i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 20/32
case i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 21/32
case i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 22/32
case i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 23/32
case i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 24/32
case i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 25/32
case i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 26/32
case i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 27/32
case i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 28/32
case i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 29/32
case i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 30/32
case i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 31/32
case i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
// combination 32/32
case i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
func (w *rw) Push(target string, opts *http.PushOptions) error {
f := w.w.(http.Pusher).Push
if w.h.Push != nil {
f = w.h.Push(f)
}
return f(target, opts)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}

View file

@ -0,0 +1,278 @@
// +build !go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
switch {
// combination 1/16
case !i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/16
case !i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 3/16
case !i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 4/16
case !i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 5/16
case !i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 6/16
case !i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 7/16
case !i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 8/16
case !i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 9/16
case i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 10/16
case i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 11/16
case i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 12/16
case i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 13/16
case i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 14/16
case i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 15/16
case i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 16/16
case i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}

View file

@ -1,43 +0,0 @@
language: go
go:
- 1.2.x
- 1.6.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- 1.14.x
- tip
os:
- linux
arch:
- amd64
- ppc64le
dist: xenial
env:
- GOARCH=amd64
jobs:
include:
- os: windows
go: 1.14.x
- os: osx
go: 1.14.x
- os: linux
go: 1.14.x
arch: arm64
- os: linux
go: 1.14.x
env:
- GOARCH=386
script:
- go test -v -cover ./... || go test -v ./...
matrix:
allowfailures:
go: 1.2.x

View file

@ -170,12 +170,10 @@ func PrintPacket(p *Packet) {
printPacket(os.Stdout, p, 0, false)
}
func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) {
indentStr := ""
for len(indentStr) != indent {
indentStr += " "
}
// Return a string describing packet content. This is not recursive,
// If the packet is a sequence, use `printPacket()`, or browse
// sequence yourself.
func DescribePacket(p *Packet) string {
classStr := ClassMap[p.ClassType]
@ -194,7 +192,17 @@ func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) {
description = p.Description + ": "
}
_, _ = fmt.Fprintf(out, "%s%s(%s, %s, %s) Len=%d %q\n", indentStr, description, classStr, tagTypeStr, tagStr, p.Data.Len(), value)
return fmt.Sprintf("%s(%s, %s, %s) Len=%d %q", description, classStr, tagTypeStr, tagStr, p.Data.Len(), value)
}
func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) {
indentStr := ""
for len(indentStr) != indent {
indentStr += " "
}
_, _ = fmt.Fprintf(out, "%s%s\n", indentStr, DescribePacket(p))
if printBytes {
PrintBytes(out, p.Bytes(), indentStr)
@ -317,7 +325,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
// Read the next packet
child, r, err := readPacket(reader)
if err != nil {
return nil, read, err
return nil, read, unexpectedEOF(err)
}
contentRead += r
read += r
@ -348,10 +356,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
if length > 0 {
_, err := io.ReadFull(reader, content)
if err != nil {
if err == io.EOF {
return nil, read, io.ErrUnexpectedEOF
}
return nil, read, err
return nil, read, unexpectedEOF(err)
}
read += length
}

View file

@ -37,7 +37,7 @@ func readIdentifier(reader io.Reader) (Identifier, int, error) {
if Debug {
fmt.Printf("error reading high-tag-number tag byte %d: %v\n", tagBytes, err)
}
return Identifier{}, read, err
return Identifier{}, read, unexpectedEOF(err)
}
tagBytes++
read++

View file

@ -13,7 +13,7 @@ func readLength(reader io.Reader) (length int, read int, err error) {
if Debug {
fmt.Printf("error reading length byte: %v\n", err)
}
return 0, 0, err
return 0, 0, unexpectedEOF(err)
}
read++
@ -47,7 +47,7 @@ func readLength(reader io.Reader) (length int, read int, err error) {
if Debug {
fmt.Printf("error reading long-form length byte %d: %v\n", i, err)
}
return 0, read, err
return 0, read, unexpectedEOF(err)
}
read++

View file

@ -89,12 +89,18 @@ func parseBinaryFloat(v []byte) (float64, error) {
case 0x02:
expLen = 3
case 0x03:
if len(v) < 2 {
return 0.0, errors.New("invalid data")
}
expLen = int(v[0])
if expLen > 8 {
return 0.0, errors.New("too big value of exponent")
}
v = v[1:]
}
if expLen > len(v) {
return 0.0, errors.New("too big value of exponent")
}
buf, v = v[:expLen], v[expLen:]
exponent, err := ParseInt64(buf)
if err != nil {

View file

@ -6,14 +6,18 @@ func readByte(reader io.Reader) (byte, error) {
bytes := make([]byte, 1)
_, err := io.ReadFull(reader, bytes)
if err != nil {
if err == io.EOF {
return 0, io.ErrUnexpectedEOF
}
return 0, err
}
return bytes[0], nil
}
func unexpectedEOF(err error) error {
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
}
func isEOCPacket(p *Packet) bool {
return p != nil &&
p.Tag == TagEOC &&

View file

@ -1,8 +1,7 @@
package ldap
import (
"log"
"fmt"
ber "github.com/go-asn1-ber/asn1-ber"
)
@ -63,7 +62,6 @@ func NewAddRequest(dn string, controls []Control) *AddRequest {
DN: dn,
Controls: controls,
}
}
// Add performs the given AddRequest
@ -85,7 +83,7 @@ func (l *Conn) Add(addRequest *AddRequest) error {
return err
}
} else {
log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag)
}
return nil
}

View file

@ -261,7 +261,7 @@ func parseParams(str string) (map[string]string, error) {
var state int
for i := 0; i <= len(str); i++ {
switch state {
case 0: //reading key
case 0: // reading key
if i == len(str) {
return nil, fmt.Errorf("syntax error on %d", i)
}
@ -270,7 +270,7 @@ func parseParams(str string) (map[string]string, error) {
continue
}
state = 1
case 1: //reading value
case 1: // reading value
if i == len(str) {
m[key] = value
break
@ -289,7 +289,7 @@ func parseParams(str string) (map[string]string, error) {
default:
value += string(str[i])
}
case 2: //inside quotes
case 2: // inside quotes
if i == len(str) {
return nil, fmt.Errorf("syntax error on %d", i)
}
@ -399,6 +399,9 @@ type NTLMBindRequest struct {
Username string
// Password is the credentials to bind with
Password string
// AllowEmptyPassword sets whether the client allows binding with an empty password
// (normally used for unauthenticated bind).
AllowEmptyPassword bool
// Hash is the hex NTLM hash to bind with. Password or hash must be provided
Hash string
// Controls are optional controls to send with the bind request
@ -442,6 +445,22 @@ func (l *Conn) NTLMBind(domain, username, password string) error {
return err
}
// NTLMUnauthenticatedBind performs an bind with an empty password.
//
// A username is required. The anonymous bind is not (yet) supported by the go-ntlmssp library (https://github.com/Azure/go-ntlmssp/blob/819c794454d067543bc61d29f61fef4b3c3df62c/authenticate_message.go#L87)
//
// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 part 3.2.5.1.2
func (l *Conn) NTLMUnauthenticatedBind(domain, username string) error {
req := &NTLMBindRequest{
Domain: domain,
Username: username,
Password: "",
AllowEmptyPassword: true,
}
_, err := l.NTLMChallengeBind(req)
return err
}
// NTLMBindWithHash performs an NTLM Bind with an NTLM hash instead of plaintext password (pass-the-hash)
func (l *Conn) NTLMBindWithHash(domain, username, hash string) error {
req := &NTLMBindRequest{
@ -455,7 +474,7 @@ func (l *Conn) NTLMBindWithHash(domain, username, hash string) error {
// NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request
func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) {
if ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" {
if !ntlmBindRequest.AllowEmptyPassword && ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" {
return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client"))
}
@ -496,10 +515,11 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes
var err error
var responseMessage []byte
// generate a response message to the challenge with the given Username/Password if password is provided
if ntlmBindRequest.Password != "" {
responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password)
} else if ntlmBindRequest.Hash != "" {
if ntlmBindRequest.Hash != "" {
responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash)
} else if ntlmBindRequest.Password != "" || ntlmBindRequest.AllowEmptyPassword {
_, _, domainNeeded := ntlmssp.GetDomain(ntlmBindRequest.Username)
responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password, domainNeeded)
} else {
err = fmt.Errorf("need a password or hash to generate reply")
}
@ -538,3 +558,178 @@ func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindRes
err = GetLDAPError(packet)
return result, err
}
// GSSAPIClient interface is used as the client-side implementation for the
// GSSAPI SASL mechanism.
// Interface inspired by GSSAPIClient from golang.org/x/crypto/ssh
type GSSAPIClient interface {
// InitSecContext initiates the establishment of a security context for
// GSS-API between the client and server.
// Initially the token parameter should be specified as nil.
// The routine may return a outputToken which should be transferred to
// the server, where the server will present it to AcceptSecContext.
// If no token need be sent, InitSecContext will indicate this by setting
// needContinue to false. To complete the context
// establishment, one or more reply tokens may be required from the server;
// if so, InitSecContext will return a needContinue which is true.
// In this case, InitSecContext should be called again when the
// reply token is received from the server, passing the reply token
// to InitSecContext via the token parameters.
// See RFC 4752 section 3.1.
InitSecContext(target string, token []byte) (outputToken []byte, needContinue bool, err error)
// NegotiateSaslAuth performs the last step of the Sasl handshake.
// It takes a token, which, when unwrapped, describes the servers supported
// security layers (first octet) and maximum receive buffer (remaining
// three octets).
// If the received token is unacceptable an error must be returned to abort
// the handshake.
// Outputs a signed token describing the client's selected security layer
// and receive buffer size and optionally an authorization identity.
// The returned token will be sent to the server and the handshake considered
// completed successfully and the server authenticated.
// See RFC 4752 section 3.1.
NegotiateSaslAuth(token []byte, authzid string) ([]byte, error)
// DeleteSecContext destroys any established secure context.
DeleteSecContext() error
}
// GSSAPIBindRequest represents a GSSAPI SASL mechanism bind request.
// See rfc4752 and rfc4513 section 5.2.1.2.
type GSSAPIBindRequest struct {
// Service Principal Name user for the service ticket. Eg. "ldap/<host>"
ServicePrincipalName string
// (Optional) Authorization entity
AuthZID string
// (Optional) Controls to send with the bind request
Controls []Control
}
// GSSAPIBind performs the GSSAPI SASL bind using the provided GSSAPI client.
func (l *Conn) GSSAPIBind(client GSSAPIClient, servicePrincipal, authzid string) error {
return l.GSSAPIBindRequest(client, &GSSAPIBindRequest{
ServicePrincipalName: servicePrincipal,
AuthZID: authzid,
})
}
// GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client.
func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error {
//nolint:errcheck
defer client.DeleteSecContext()
var err error
var reqToken []byte
var recvToken []byte
needInit := true
for {
if needInit {
// Establish secure context between client and server.
reqToken, needInit, err = client.InitSecContext(req.ServicePrincipalName, recvToken)
if err != nil {
return err
}
} else {
// Secure context is set up, perform the last step of SASL handshake.
reqToken, err = client.NegotiateSaslAuth(recvToken, req.AuthZID)
if err != nil {
return err
}
}
// Send Bind request containing the current token and extract the
// token sent by server.
recvToken, err = l.saslBindTokenExchange(req.Controls, reqToken)
if err != nil {
return err
}
if !needInit && len(recvToken) == 0 {
break
}
}
return nil
}
func (l *Conn) saslBindTokenExchange(reqControls []Control, reqToken []byte) ([]byte, error) {
// Construct LDAP Bind request with GSSAPI SASL mechanism.
envelope := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
envelope.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID"))
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))
auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication")
auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "GSSAPI", "SASL Mech"))
if len(reqToken) > 0 {
auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(reqToken), "Credentials"))
}
request.AppendChild(auth)
envelope.AppendChild(request)
if len(reqControls) > 0 {
envelope.AppendChild(encodeControls(reqControls))
}
msgCtx, err := l.sendMessage(envelope)
if err != nil {
return nil, err
}
defer l.finishMessage(msgCtx)
packet, err := l.readPacket(msgCtx)
if err != nil {
return nil, err
}
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
if l.Debug {
if err = addLDAPDescriptions(packet); err != nil {
return nil, err
}
ber.PrintPacket(packet)
}
// https://www.rfc-editor.org/rfc/rfc4511#section-4.1.1
// packet is an envelope
// child 0 is message id
// child 1 is protocolOp
if len(packet.Children) != 2 {
return nil, fmt.Errorf("bad bind response")
}
protocolOp := packet.Children[1]
RESP:
switch protocolOp.Description {
case "Bind Response": // Bind Response
// Bind Reponse is an LDAP Response (https://www.rfc-editor.org/rfc/rfc4511#section-4.1.9)
// with an additional optional serverSaslCreds string (https://www.rfc-editor.org/rfc/rfc4511#section-4.2.2)
// child 0 is resultCode
resultCode := protocolOp.Children[0]
if resultCode.Tag != ber.TagEnumerated {
break RESP
}
switch resultCode.Value.(int64) {
case 14: // Sasl bind in progress
if len(protocolOp.Children) < 3 {
break RESP
}
referral := protocolOp.Children[3]
switch referral.Description {
case "Referral":
if referral.ClassType != ber.ClassContext || referral.Tag != ber.TagObjectDescriptor {
break RESP
}
return ioutil.ReadAll(referral.Data)
}
// Optional:
//if len(protocolOp.Children) == 4 {
// serverSaslCreds := protocolOp.Children[4]
//}
case 0: // Success - Bind OK.
// SASL layer in effect (if any) (See https://www.rfc-editor.org/rfc/rfc4513#section-5.2.1.4)
// NOTE: SASL security layers are not supported currently.
return nil, nil
}
}
return nil, GetLDAPError(packet)
}

View file

@ -1,6 +1,7 @@
package ldap
import (
"context"
"crypto/tls"
"time"
)
@ -9,14 +10,18 @@ import (
type Client interface {
Start()
StartTLS(*tls.Config) error
Close()
Close() error
GetLastError() error
IsClosing() bool
SetTimeout(time.Duration)
TLSConnectionState() (tls.ConnectionState, bool)
Bind(username, password string) error
UnauthenticatedBind(username string) error
SimpleBind(*SimpleBindRequest) (*SimpleBindResult, error)
ExternalBind() error
NTLMUnauthenticatedBind(domain, username string) error
Unbind() error
Add(*AddRequest) error
Del(*DelRequest) error
@ -28,5 +33,9 @@ type Client interface {
PasswordModify(*PasswordModifyRequest) (*PasswordModifyResult, error)
Search(*SearchRequest) (*SearchResult, error)
SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response
SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error)
DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error)
DirSyncAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) Response
Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool) Response
}

View file

@ -34,7 +34,8 @@ func (l *Conn) Compare(dn, attribute, value string) (bool, error) {
msgCtx, err := l.doRequest(&CompareRequest{
DN: dn,
Attribute: attribute,
Value: value})
Value: value,
})
if err != nil {
return false, err
}

View file

@ -2,10 +2,10 @@ package ldap
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"net/url"
"sync"
@ -61,13 +61,21 @@ type messageContext struct {
// sendResponse should only be called within the processMessages() loop which
// is also responsible for closing the responses channel.
func (msgCtx *messageContext) sendResponse(packet *PacketResponse) {
func (msgCtx *messageContext) sendResponse(packet *PacketResponse, timeout time.Duration) {
timeoutCtx := context.Background()
if timeout > 0 {
var cancelFunc context.CancelFunc
timeoutCtx, cancelFunc = context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
}
select {
case msgCtx.responses <- packet:
// Successfully sent packet to message handler.
case <-msgCtx.done:
// The request handler is done and will not receive more
// packets.
case <-timeoutCtx.Done():
// The timeout was reached before the packet was sent.
}
}
@ -88,6 +96,7 @@ const (
type Conn struct {
// requestTimeout is loaded atomically
// so we need to ensure 64-bit alignment on 32-bit platforms.
// https://github.com/go-ldap/ldap/pull/199
requestTimeout int64
conn net.Conn
isTLS bool
@ -102,6 +111,8 @@ type Conn struct {
wgClose sync.WaitGroup
outstandingRequests uint
messageMutex sync.Mutex
err error
}
var _ Client = &Conn{}
@ -119,30 +130,31 @@ type DialOpt func(*DialContext)
// DialWithDialer updates net.Dialer in DialContext.
func DialWithDialer(d *net.Dialer) DialOpt {
return func(dc *DialContext) {
dc.d = d
dc.dialer = d
}
}
// DialWithTLSConfig updates tls.Config in DialContext.
func DialWithTLSConfig(tc *tls.Config) DialOpt {
return func(dc *DialContext) {
dc.tc = tc
dc.tlsConfig = tc
}
}
// DialWithTLSDialer is a wrapper for DialWithTLSConfig with the option to
// specify a net.Dialer to for example define a timeout or a custom resolver.
// @deprecated Use DialWithDialer and DialWithTLSConfig instead
func DialWithTLSDialer(tlsConfig *tls.Config, dialer *net.Dialer) DialOpt {
return func(dc *DialContext) {
dc.tc = tlsConfig
dc.d = dialer
dc.tlsConfig = tlsConfig
dc.dialer = dialer
}
}
// DialContext contains necessary parameters to dial the given ldap URL.
type DialContext struct {
d *net.Dialer
tc *tls.Config
dialer *net.Dialer
tlsConfig *tls.Config
}
func (dc *DialContext) dial(u *url.URL) (net.Conn, error) {
@ -150,7 +162,7 @@ func (dc *DialContext) dial(u *url.URL) (net.Conn, error) {
if u.Path == "" || u.Path == "/" {
u.Path = "/var/run/slapd/ldapi"
}
return dc.d.Dial("unix", u.Path)
return dc.dialer.Dial("unix", u.Path)
}
host, port, err := net.SplitHostPort(u.Host)
@ -161,16 +173,21 @@ func (dc *DialContext) dial(u *url.URL) (net.Conn, error) {
}
switch u.Scheme {
case "cldap":
if port == "" {
port = DefaultLdapPort
}
return dc.dialer.Dial("udp", net.JoinHostPort(host, port))
case "ldap":
if port == "" {
port = DefaultLdapPort
}
return dc.d.Dial("tcp", net.JoinHostPort(host, port))
return dc.dialer.Dial("tcp", net.JoinHostPort(host, port))
case "ldaps":
if port == "" {
port = DefaultLdapsPort
}
return tls.DialWithDialer(dc.d, "tcp", net.JoinHostPort(host, port), dc.tc)
return tls.DialWithDialer(dc.dialer, "tcp", net.JoinHostPort(host, port), dc.tlsConfig)
}
return nil, fmt.Errorf("Unknown scheme '%s'", u.Scheme)
@ -203,7 +220,8 @@ func DialTLS(network, addr string, config *tls.Config) (*Conn, error) {
}
// DialURL connects to the given ldap URL.
// The following schemas are supported: ldap://, ldaps://, ldapi://.
// The following schemas are supported: ldap://, ldaps://, ldapi://,
// and cldap:// (RFC1798, deprecated but used by Active Directory).
// On success a new Conn for the connection is returned.
func DialURL(addr string, opts ...DialOpt) (*Conn, error) {
u, err := url.Parse(addr)
@ -215,8 +233,8 @@ func DialURL(addr string, opts ...DialOpt) (*Conn, error) {
for _, opt := range opts {
opt(&dc)
}
if dc.d == nil {
dc.d = &net.Dialer{Timeout: DefaultTimeout}
if dc.dialer == nil {
dc.dialer = &net.Dialer{Timeout: DefaultTimeout}
}
c, err := dc.dial(u)
@ -231,7 +249,7 @@ func DialURL(addr string, opts ...DialOpt) (*Conn, error) {
// NewConn returns a new Conn using conn for network I/O.
func NewConn(conn net.Conn, isTLS bool) *Conn {
return &Conn{
l := &Conn{
conn: conn,
chanConfirm: make(chan struct{}),
chanMessageID: make(chan int64),
@ -240,11 +258,12 @@ func NewConn(conn net.Conn, isTLS bool) *Conn {
requestTimeout: 0,
isTLS: isTLS,
}
l.wgClose.Add(1)
return l
}
// Start initializes goroutines to read responses and process messages
func (l *Conn) Start() {
l.wgClose.Add(1)
go l.reader()
go l.processMessages()
}
@ -260,31 +279,45 @@ func (l *Conn) setClosing() bool {
}
// Close closes the connection.
func (l *Conn) Close() {
func (l *Conn) Close() (err error) {
l.messageMutex.Lock()
defer l.messageMutex.Unlock()
if l.setClosing() {
l.Debug.Printf("Sending quit message and waiting for confirmation")
l.chanMessage <- &messagePacket{Op: MessageQuit}
<-l.chanConfirm
timeoutCtx := context.Background()
if l.getTimeout() > 0 {
var cancelFunc context.CancelFunc
timeoutCtx, cancelFunc = context.WithTimeout(timeoutCtx, time.Duration(l.getTimeout()))
defer cancelFunc()
}
select {
case <-l.chanConfirm:
// Confirmation was received.
case <-timeoutCtx.Done():
// The timeout was reached before confirmation was received.
}
close(l.chanMessage)
l.Debug.Printf("Closing network connection")
if err := l.conn.Close(); err != nil {
log.Println(err)
}
err = l.conn.Close()
l.wgClose.Done()
}
l.wgClose.Wait()
return err
}
// SetTimeout sets the time after a request is sent that a MessageTimeout triggers
func (l *Conn) SetTimeout(timeout time.Duration) {
if timeout > 0 {
atomic.StoreInt64(&l.requestTimeout, int64(timeout))
}
atomic.StoreInt64(&l.requestTimeout, int64(timeout))
}
func (l *Conn) getTimeout() int64 {
return atomic.LoadInt64(&l.requestTimeout)
}
// Returns the next available messageID
@ -295,6 +328,14 @@ func (l *Conn) nextMessageID() int64 {
return 0
}
// GetLastError returns the last recorded error from goroutines like processMessages and reader.
// Only the last recorded error will be returned.
func (l *Conn) GetLastError() error {
l.messageMutex.Lock()
defer l.messageMutex.Unlock()
return l.err
}
// StartTLS sends the command to start a TLS session and then creates a new TLS Client
func (l *Conn) StartTLS(config *tls.Config) error {
if l.isTLS {
@ -443,13 +484,13 @@ func (l *Conn) sendProcessMessage(message *messagePacket) bool {
func (l *Conn) processMessages() {
defer func() {
if err := recover(); err != nil {
log.Printf("ldap: recovered panic in processMessages: %v", err)
l.err = fmt.Errorf("ldap: recovered panic in processMessages: %v", err)
}
for messageID, msgCtx := range l.messageContexts {
// If we are closing due to an error, inform anyone who
// is waiting about the error.
if l.IsClosing() && l.closeErr.Load() != nil {
msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)})
msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}, time.Duration(l.getTimeout()))
}
l.Debug.Printf("Closing channel for MessageID %d", messageID)
close(msgCtx.responses)
@ -477,7 +518,7 @@ func (l *Conn) processMessages() {
_, err := l.conn.Write(buf)
if err != nil {
l.Debug.Printf("Error Sending Message: %s", err.Error())
message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)})
message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}, time.Duration(l.getTimeout()))
close(message.Context.responses)
break
}
@ -487,28 +528,35 @@ func (l *Conn) processMessages() {
l.messageContexts[message.MessageID] = message.Context
// Add timeout if defined
requestTimeout := time.Duration(atomic.LoadInt64(&l.requestTimeout))
requestTimeout := l.getTimeout()
if requestTimeout > 0 {
go func() {
timer := time.NewTimer(time.Duration(requestTimeout))
defer func() {
if err := recover(); err != nil {
log.Printf("ldap: recovered panic in RequestTimeout: %v", err)
l.err = fmt.Errorf("ldap: recovered panic in RequestTimeout: %v", err)
}
timer.Stop()
}()
time.Sleep(requestTimeout)
timeoutMessage := &messagePacket{
Op: MessageTimeout,
MessageID: message.MessageID,
select {
case <-timer.C:
timeoutMessage := &messagePacket{
Op: MessageTimeout,
MessageID: message.MessageID,
}
l.sendProcessMessage(timeoutMessage)
case <-message.Context.done:
}
l.sendProcessMessage(timeoutMessage)
}()
}
case MessageResponse:
l.Debug.Printf("Receiving message %d", message.MessageID)
if msgCtx, ok := l.messageContexts[message.MessageID]; ok {
msgCtx.sendResponse(&PacketResponse{message.Packet, nil})
msgCtx.sendResponse(&PacketResponse{message.Packet, nil}, time.Duration(l.getTimeout()))
} else {
log.Printf("Received unexpected message %d, %v", message.MessageID, l.IsClosing())
l.err = fmt.Errorf("ldap: received unexpected message %d, %v", message.MessageID, l.IsClosing())
l.Debug.PrintPacket(message.Packet)
}
case MessageTimeout:
@ -516,7 +564,7 @@ func (l *Conn) processMessages() {
// All reads will return immediately
if msgCtx, ok := l.messageContexts[message.MessageID]; ok {
l.Debug.Printf("Receiving message timeout for %d", message.MessageID)
msgCtx.sendResponse(&PacketResponse{message.Packet, NewError(ErrorNetwork, errors.New("ldap: connection timed out"))})
msgCtx.sendResponse(&PacketResponse{message.Packet, NewError(ErrorNetwork, errors.New("ldap: connection timed out"))}, time.Duration(l.getTimeout()))
delete(l.messageContexts, message.MessageID)
close(msgCtx.responses)
}
@ -535,7 +583,7 @@ func (l *Conn) reader() {
cleanstop := false
defer func() {
if err := recover(); err != nil {
log.Printf("ldap: recovered panic in reader: %v", err)
l.err = fmt.Errorf("ldap: recovered panic in reader: %v", err)
}
if !cleanstop {
l.Close()

View file

@ -5,6 +5,7 @@ import (
"strconv"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/google/uuid"
)
const (
@ -20,6 +21,13 @@ const (
ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2"
// ControlTypeWhoAmI - https://tools.ietf.org/html/rfc4532
ControlTypeWhoAmI = "1.3.6.1.4.1.4203.1.11.3"
// ControlTypeSubtreeDelete - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02
ControlTypeSubtreeDelete = "1.2.840.113556.1.4.805"
// ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt
ControlTypeServerSideSorting = "1.2.840.113556.1.4.473"
// ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt
ControlTypeServerSideSortingResult = "1.2.840.113556.1.4.474"
// ControlTypeMicrosoftNotification - https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx
ControlTypeMicrosoftNotification = "1.2.840.113556.1.4.528"
@ -27,16 +35,43 @@ const (
ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417"
// ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN
ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309"
// ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx
ControlTypeDirSync = "1.2.840.113556.1.4.841"
// ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt
ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1"
// ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt
ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2"
// ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt
ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3"
// ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt
ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4"
)
// Flags for DirSync control
const (
DirSyncIncrementalValues int64 = 2147483648
DirSyncPublicDataOnly int64 = 8192
DirSyncAncestorsFirstOrder int64 = 2048
DirSyncObjectSecurity int64 = 1
)
// ControlTypeMap maps controls to text descriptions
var ControlTypeMap = map[string]string{
ControlTypePaging: "Paging",
ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
ControlTypeManageDsaIT: "Manage DSA IT",
ControlTypeMicrosoftNotification: "Change Notification - Microsoft",
ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft",
ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft",
ControlTypePaging: "Paging",
ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
ControlTypeManageDsaIT: "Manage DSA IT",
ControlTypeSubtreeDelete: "Subtree Delete Control",
ControlTypeMicrosoftNotification: "Change Notification - Microsoft",
ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft",
ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft",
ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)",
ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)",
ControlTypeDirSync: "DirSync",
ControlTypeSyncRequest: "Sync Request",
ControlTypeSyncState: "Sync State",
ControlTypeSyncDone: "Sync Done",
ControlTypeSyncInfo: "Sync Info",
}
// Control defines an interface controls provide to encode and describe themselves
@ -229,7 +264,7 @@ func (c *ControlManageDsaIT) GetControlType() string {
// Encode returns the ber packet representation
func (c *ControlManageDsaIT) Encode() *ber.Packet {
//FIXME
// FIXME
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")"))
if c.Criticality {
@ -369,7 +404,13 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
case 2:
packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")"
ControlType = packet.Children[0].Value.(string)
if packet.Children[0].Value != nil {
ControlType = packet.Children[0].Value.(string)
} else if packet.Children[0].Data != nil {
ControlType = packet.Children[0].Data.String()
} else {
return nil, fmt.Errorf("not found where to get the control type")
}
// Children[1] could be criticality or value (both are optional)
// duck-type on whether this is a boolean
@ -436,18 +477,18 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
for _, child := range sequence.Children {
if child.Tag == 0 {
//Warning
// Warning
warningPacket := child.Children[0]
val, err := ber.ParseInt64(warningPacket.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
if warningPacket.Tag == 0 {
//timeBeforeExpiration
// timeBeforeExpiration
c.Expire = val
warningPacket.Value = c.Expire
} else if warningPacket.Tag == 1 {
//graceAuthNsRemaining
// graceAuthNsRemaining
c.Grace = val
warningPacket.Value = c.Grace
}
@ -485,6 +526,36 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
return NewControlMicrosoftShowDeleted(), nil
case ControlTypeMicrosoftServerLinkTTL:
return NewControlMicrosoftServerLinkTTL(), nil
case ControlTypeSubtreeDelete:
return NewControlSubtreeDelete(), nil
case ControlTypeServerSideSorting:
return NewControlServerSideSorting(value)
case ControlTypeServerSideSortingResult:
return NewControlServerSideSortingResult(value)
case ControlTypeDirSync:
value.Description += " (DirSync)"
return NewResponseControlDirSync(value)
case ControlTypeSyncState:
value.Description += " (Sync State)"
valueChildren, err := ber.DecodePacketErr(value.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
return NewControlSyncState(valueChildren)
case ControlTypeSyncDone:
value.Description += " (Sync Done)"
valueChildren, err := ber.DecodePacketErr(value.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
return NewControlSyncDone(valueChildren)
case ControlTypeSyncInfo:
value.Description += " (Sync Info)"
valueChildren, err := ber.DecodePacketErr(value.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
return NewControlSyncInfo(valueChildren)
default:
c := new(ControlString)
c.ControlType = ControlType
@ -519,6 +590,35 @@ func NewControlBeheraPasswordPolicy() *ControlBeheraPasswordPolicy {
}
}
// ControlSubtreeDelete implements the subtree delete control described in
// https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02
type ControlSubtreeDelete struct{}
// GetControlType returns the OID
func (c *ControlSubtreeDelete) GetControlType() string {
return ControlTypeSubtreeDelete
}
// NewControlSubtreeDelete returns a ControlSubtreeDelete control.
func NewControlSubtreeDelete() *ControlSubtreeDelete {
return &ControlSubtreeDelete{}
}
// Encode returns the ber packet representation
func (c *ControlSubtreeDelete) Encode() *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSubtreeDelete, "Control Type ("+ControlTypeMap[ControlTypeSubtreeDelete]+")"))
return packet
}
func (c *ControlSubtreeDelete) String() string {
return fmt.Sprintf(
"Control Type: %s (%q)",
ControlTypeMap[ControlTypeSubtreeDelete],
ControlTypeSubtreeDelete)
}
func encodeControls(controls []Control) *ber.Packet {
packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls")
for _, control := range controls {
@ -526,3 +626,669 @@ func encodeControls(controls []Control) *ber.Packet {
}
return packet
}
// ControlDirSync implements the control described in https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx
type ControlDirSync struct {
Criticality bool
Flags int64
MaxAttrCount int64
Cookie []byte
}
// @deprecated Use NewRequestControlDirSync instead
func NewControlDirSync(flags int64, maxAttrCount int64, cookie []byte) *ControlDirSync {
return NewRequestControlDirSync(flags, maxAttrCount, cookie)
}
// NewRequestControlDirSync returns a dir sync control
func NewRequestControlDirSync(
flags int64, maxAttrCount int64, cookie []byte,
) *ControlDirSync {
return &ControlDirSync{
Criticality: true,
Flags: flags,
MaxAttrCount: maxAttrCount,
Cookie: cookie,
}
}
// NewResponseControlDirSync returns a dir sync control
func NewResponseControlDirSync(value *ber.Packet) (*ControlDirSync, error) {
if value.Value != nil {
valueChildren, err := ber.DecodePacketErr(value.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
value.Data.Truncate(0)
value.Value = nil
value.AppendChild(valueChildren)
}
child := value.Children[0]
if len(child.Children) != 3 { // also on initial creation, Cookie is an empty string
return nil, fmt.Errorf("invalid number of children in dirSync control")
}
child.Description = "DirSync Control Value"
child.Children[0].Description = "Flags"
child.Children[1].Description = "MaxAttrCount"
child.Children[2].Description = "Cookie"
cookie := child.Children[2].Data.Bytes()
child.Children[2].Value = cookie
return &ControlDirSync{
Criticality: true,
Flags: child.Children[0].Value.(int64),
MaxAttrCount: child.Children[1].Value.(int64),
Cookie: cookie,
}, nil
}
// GetControlType returns the OID
func (c *ControlDirSync) GetControlType() string {
return ControlTypeDirSync
}
// String returns a human-readable description
func (c *ControlDirSync) String() string {
return fmt.Sprintf(
"ControlType: %s (%q) Criticality: %t ControlValue: Flags: %d MaxAttrCount: %d",
ControlTypeMap[ControlTypeDirSync],
ControlTypeDirSync,
c.Criticality,
c.Flags,
c.MaxAttrCount,
)
}
// Encode returns the ber packet representation
func (c *ControlDirSync) Encode() *ber.Packet {
cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Cookie")
if len(c.Cookie) != 0 {
cookie.Value = c.Cookie
cookie.Data.Write(c.Cookie)
}
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeDirSync, "Control Type ("+ControlTypeMap[ControlTypeDirSync]+")"))
packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) // must be true always
val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)")
seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value")
seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.Flags), "Flags"))
seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.MaxAttrCount), "MaxAttrCount"))
seq.AppendChild(cookie)
val.AppendChild(seq)
packet.AppendChild(val)
return packet
}
// SetCookie stores the given cookie in the dirSync control
func (c *ControlDirSync) SetCookie(cookie []byte) {
c.Cookie = cookie
}
// ControlServerSideSorting
type SortKey struct {
Reverse bool
AttributeType string
MatchingRule string
}
type ControlServerSideSorting struct {
SortKeys []*SortKey
}
func (c *ControlServerSideSorting) GetControlType() string {
return ControlTypeServerSideSorting
}
func NewControlServerSideSorting(value *ber.Packet) (*ControlServerSideSorting, error) {
sortKeys := []*SortKey{}
val := value.Children[1].Children
if len(val) != 1 {
return nil, fmt.Errorf("no sequence value in packet")
}
sequences := val[0].Children
for i, sequence := range sequences {
sortKey := &SortKey{}
if len(sequence.Children) < 2 {
return nil, fmt.Errorf("attributeType or matchingRule is missing from sequence %d", i)
}
sortKey.AttributeType = sequence.Children[0].Value.(string)
sortKey.MatchingRule = sequence.Children[1].Value.(string)
if len(sequence.Children) == 3 {
sortKey.Reverse = sequence.Children[2].Value.(bool)
}
sortKeys = append(sortKeys, sortKey)
}
return &ControlServerSideSorting{SortKeys: sortKeys}, nil
}
func NewControlServerSideSortingWithSortKeys(sortKeys []*SortKey) *ControlServerSideSorting {
return &ControlServerSideSorting{SortKeys: sortKeys}
}
func (c *ControlServerSideSorting) Encode() *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
control := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.GetControlType(), "Control Type")
value := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value")
seqs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortKeyList")
for _, f := range c.SortKeys {
seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "")
seq.AppendChild(
ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, f.AttributeType, "attributeType"),
)
seq.AppendChild(
ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, f.MatchingRule, "orderingRule"),
)
if f.Reverse {
seq.AppendChild(
ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 1, f.Reverse, "reverseOrder"),
)
}
seqs.AppendChild(seq)
}
value.AppendChild(seqs)
packet.AppendChild(control)
packet.AppendChild(value)
return packet
}
func (c *ControlServerSideSorting) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality:%t %+v",
"Server Side Sorting",
c.GetControlType(),
false,
c.SortKeys,
)
}
// ControlServerSideSortingResponse
const (
ControlServerSideSortingCodeSuccess ControlServerSideSortingCode = 0
ControlServerSideSortingCodeOperationsError ControlServerSideSortingCode = 1
ControlServerSideSortingCodeTimeLimitExceeded ControlServerSideSortingCode = 2
ControlServerSideSortingCodeStrongAuthRequired ControlServerSideSortingCode = 8
ControlServerSideSortingCodeAdminLimitExceeded ControlServerSideSortingCode = 11
ControlServerSideSortingCodeNoSuchAttribute ControlServerSideSortingCode = 16
ControlServerSideSortingCodeInappropriateMatching ControlServerSideSortingCode = 18
ControlServerSideSortingCodeInsufficientAccessRights ControlServerSideSortingCode = 50
ControlServerSideSortingCodeBusy ControlServerSideSortingCode = 51
ControlServerSideSortingCodeUnwillingToPerform ControlServerSideSortingCode = 53
ControlServerSideSortingCodeOther ControlServerSideSortingCode = 80
)
var ControlServerSideSortingCodes = []ControlServerSideSortingCode{
ControlServerSideSortingCodeSuccess,
ControlServerSideSortingCodeOperationsError,
ControlServerSideSortingCodeTimeLimitExceeded,
ControlServerSideSortingCodeStrongAuthRequired,
ControlServerSideSortingCodeAdminLimitExceeded,
ControlServerSideSortingCodeNoSuchAttribute,
ControlServerSideSortingCodeInappropriateMatching,
ControlServerSideSortingCodeInsufficientAccessRights,
ControlServerSideSortingCodeBusy,
ControlServerSideSortingCodeUnwillingToPerform,
ControlServerSideSortingCodeOther,
}
type ControlServerSideSortingCode int64
// Valid test the code contained in the control against the ControlServerSideSortingCodes slice and return an error if the code is unknown.
func (c ControlServerSideSortingCode) Valid() error {
for _, validRet := range ControlServerSideSortingCodes {
if c == validRet {
return nil
}
}
return fmt.Errorf("unknown return code : %d", c)
}
func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSortingResult, error) {
control := &ControlServerSideSortingResult{}
if pkt == nil || len(pkt.Children) == 0 {
return nil, fmt.Errorf("bad packet")
}
codeInt, err := ber.ParseInt64(pkt.Children[0].Data.Bytes())
if err != nil {
return nil, err
}
code := ControlServerSideSortingCode(codeInt)
if err := code.Valid(); err != nil {
return nil, err
}
return control, nil
}
type ControlServerSideSortingResult struct {
Criticality bool
Result ControlServerSideSortingCode
// Not populated for now. I can't get openldap to send me this value, so I think this is specific to other directory server
// AttributeType string
}
func (control *ControlServerSideSortingResult) GetControlType() string {
return ControlTypeServerSideSortingResult
}
func (c *ControlServerSideSortingResult) Encode() *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortResult sequence")
sortResult := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(c.Result), "SortResult")
packet.AppendChild(sortResult)
return packet
}
func (c *ControlServerSideSortingResult) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality:%t ResultCode:%+v",
"Server Side Sorting Result",
c.GetControlType(),
c.Criticality,
c.Result,
)
}
// Mode for ControlTypeSyncRequest
type ControlSyncRequestMode int64
const (
SyncRequestModeRefreshOnly ControlSyncRequestMode = 1
SyncRequestModeRefreshAndPersist ControlSyncRequestMode = 3
)
// ControlSyncRequest implements the Sync Request Control described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncRequest struct {
Criticality bool
Mode ControlSyncRequestMode
Cookie []byte
ReloadHint bool
}
func NewControlSyncRequest(
mode ControlSyncRequestMode, cookie []byte, reloadHint bool,
) *ControlSyncRequest {
return &ControlSyncRequest{
Criticality: true,
Mode: mode,
Cookie: cookie,
ReloadHint: reloadHint,
}
}
// GetControlType returns the OID
func (c *ControlSyncRequest) GetControlType() string {
return ControlTypeSyncRequest
}
// Encode encodes the control
func (c *ControlSyncRequest) Encode() *ber.Packet {
_mode := int64(c.Mode)
mode := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, _mode, "Mode")
var cookie *ber.Packet
if len(c.Cookie) > 0 {
cookie = ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie")
cookie.Value = c.Cookie
cookie.Data.Write(c.Cookie)
}
reloadHint := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.ReloadHint, "Reload Hint")
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSyncRequest, "Control Type ("+ControlTypeMap[ControlTypeSyncRequest]+")"))
packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality"))
val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Sync Request)")
seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Sync Request Value")
seq.AppendChild(mode)
if cookie != nil {
seq.AppendChild(cookie)
}
seq.AppendChild(reloadHint)
val.AppendChild(seq)
packet.AppendChild(val)
return packet
}
// String returns a human-readable description
func (c *ControlSyncRequest) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality: %t Mode: %d Cookie: %s ReloadHint: %t",
ControlTypeMap[ControlTypeSyncRequest],
ControlTypeSyncRequest,
c.Criticality,
c.Mode,
string(c.Cookie),
c.ReloadHint,
)
}
// State for ControlSyncState
type ControlSyncStateState int64
const (
SyncStatePresent ControlSyncStateState = 0
SyncStateAdd ControlSyncStateState = 1
SyncStateModify ControlSyncStateState = 2
SyncStateDelete ControlSyncStateState = 3
)
// ControlSyncState implements the Sync State Control described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncState struct {
Criticality bool
State ControlSyncStateState
EntryUUID uuid.UUID
Cookie []byte
}
func NewControlSyncState(pkt *ber.Packet) (*ControlSyncState, error) {
var (
state ControlSyncStateState
entryUUID uuid.UUID
cookie []byte
err error
)
switch len(pkt.Children) {
case 0, 1:
return nil, fmt.Errorf("at least two children are required: %d", len(pkt.Children))
case 2:
state = ControlSyncStateState(pkt.Children[0].Value.(int64))
entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue)
if err != nil {
return nil, fmt.Errorf("failed to decode uuid: %w", err)
}
case 3:
state = ControlSyncStateState(pkt.Children[0].Value.(int64))
entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue)
if err != nil {
return nil, fmt.Errorf("failed to decode uuid: %w", err)
}
cookie = pkt.Children[2].ByteValue
}
return &ControlSyncState{
Criticality: false,
State: state,
EntryUUID: entryUUID,
Cookie: cookie,
}, nil
}
// GetControlType returns the OID
func (c *ControlSyncState) GetControlType() string {
return ControlTypeSyncState
}
// Encode encodes the control
func (c *ControlSyncState) Encode() *ber.Packet {
return nil
}
// String returns a human-readable description
func (c *ControlSyncState) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality: %t State: %d EntryUUID: %s Cookie: %s",
ControlTypeMap[ControlTypeSyncState],
ControlTypeSyncState,
c.Criticality,
c.State,
c.EntryUUID.String(),
string(c.Cookie),
)
}
// ControlSyncDone implements the Sync Done Control described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncDone struct {
Criticality bool
Cookie []byte
RefreshDeletes bool
}
func NewControlSyncDone(pkt *ber.Packet) (*ControlSyncDone, error) {
var (
cookie []byte
refreshDeletes bool
)
switch len(pkt.Children) {
case 0:
// have nothing to do
case 1:
cookie = pkt.Children[0].ByteValue
case 2:
cookie = pkt.Children[0].ByteValue
refreshDeletes = pkt.Children[1].Value.(bool)
}
return &ControlSyncDone{
Criticality: false,
Cookie: cookie,
RefreshDeletes: refreshDeletes,
}, nil
}
// GetControlType returns the OID
func (c *ControlSyncDone) GetControlType() string {
return ControlTypeSyncDone
}
// Encode encodes the control
func (c *ControlSyncDone) Encode() *ber.Packet {
return nil
}
// String returns a human-readable description
func (c *ControlSyncDone) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality: %t Cookie: %s RefreshDeletes: %t",
ControlTypeMap[ControlTypeSyncDone],
ControlTypeSyncDone,
c.Criticality,
string(c.Cookie),
c.RefreshDeletes,
)
}
// Tag For ControlSyncInfo
type ControlSyncInfoValue uint64
const (
SyncInfoNewcookie ControlSyncInfoValue = 0
SyncInfoRefreshDelete ControlSyncInfoValue = 1
SyncInfoRefreshPresent ControlSyncInfoValue = 2
SyncInfoSyncIdSet ControlSyncInfoValue = 3
)
// ControlSyncInfoNewCookie implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncInfoNewCookie struct {
Cookie []byte
}
// String returns a human-readable description
func (c *ControlSyncInfoNewCookie) String() string {
return fmt.Sprintf(
"NewCookie[Cookie: %s]",
string(c.Cookie),
)
}
// ControlSyncInfoRefreshDelete implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncInfoRefreshDelete struct {
Cookie []byte
RefreshDone bool
}
// String returns a human-readable description
func (c *ControlSyncInfoRefreshDelete) String() string {
return fmt.Sprintf(
"RefreshDelete[Cookie: %s RefreshDone: %t]",
string(c.Cookie),
c.RefreshDone,
)
}
// ControlSyncInfoRefreshPresent implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncInfoRefreshPresent struct {
Cookie []byte
RefreshDone bool
}
// String returns a human-readable description
func (c *ControlSyncInfoRefreshPresent) String() string {
return fmt.Sprintf(
"RefreshPresent[Cookie: %s RefreshDone: %t]",
string(c.Cookie),
c.RefreshDone,
)
}
// ControlSyncInfoSyncIdSet implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncInfoSyncIdSet struct {
Cookie []byte
RefreshDeletes bool
SyncUUIDs []uuid.UUID
}
// String returns a human-readable description
func (c *ControlSyncInfoSyncIdSet) String() string {
return fmt.Sprintf(
"SyncIdSet[Cookie: %s RefreshDeletes: %t SyncUUIDs: %v]",
string(c.Cookie),
c.RefreshDeletes,
c.SyncUUIDs,
)
}
// ControlSyncInfo implements the Sync Info Control described in https://www.ietf.org/rfc/rfc4533.txt
type ControlSyncInfo struct {
Criticality bool
Value ControlSyncInfoValue
NewCookie *ControlSyncInfoNewCookie
RefreshDelete *ControlSyncInfoRefreshDelete
RefreshPresent *ControlSyncInfoRefreshPresent
SyncIdSet *ControlSyncInfoSyncIdSet
}
func NewControlSyncInfo(pkt *ber.Packet) (*ControlSyncInfo, error) {
var (
cookie []byte
refreshDone = true
refreshDeletes bool
syncUUIDs []uuid.UUID
)
c := &ControlSyncInfo{Criticality: false}
switch ControlSyncInfoValue(pkt.Identifier.Tag) {
case SyncInfoNewcookie:
c.Value = SyncInfoNewcookie
c.NewCookie = &ControlSyncInfoNewCookie{
Cookie: pkt.ByteValue,
}
case SyncInfoRefreshDelete:
c.Value = SyncInfoRefreshDelete
switch len(pkt.Children) {
case 0:
// have nothing to do
case 1:
cookie = pkt.Children[0].ByteValue
case 2:
cookie = pkt.Children[0].ByteValue
refreshDone = pkt.Children[1].Value.(bool)
}
c.RefreshDelete = &ControlSyncInfoRefreshDelete{
Cookie: cookie,
RefreshDone: refreshDone,
}
case SyncInfoRefreshPresent:
c.Value = SyncInfoRefreshPresent
switch len(pkt.Children) {
case 0:
// have nothing to do
case 1:
cookie = pkt.Children[0].ByteValue
case 2:
cookie = pkt.Children[0].ByteValue
refreshDone = pkt.Children[1].Value.(bool)
}
c.RefreshPresent = &ControlSyncInfoRefreshPresent{
Cookie: cookie,
RefreshDone: refreshDone,
}
case SyncInfoSyncIdSet:
c.Value = SyncInfoSyncIdSet
switch len(pkt.Children) {
case 0:
// have nothing to do
case 1:
cookie = pkt.Children[0].ByteValue
case 2:
cookie = pkt.Children[0].ByteValue
refreshDeletes = pkt.Children[1].Value.(bool)
case 3:
cookie = pkt.Children[0].ByteValue
refreshDeletes = pkt.Children[1].Value.(bool)
syncUUIDs = make([]uuid.UUID, 0, len(pkt.Children[2].Children))
for _, child := range pkt.Children[2].Children {
u, err := uuid.FromBytes(child.ByteValue)
if err != nil {
return nil, fmt.Errorf("failed to decode uuid: %w", err)
}
syncUUIDs = append(syncUUIDs, u)
}
}
c.SyncIdSet = &ControlSyncInfoSyncIdSet{
Cookie: cookie,
RefreshDeletes: refreshDeletes,
SyncUUIDs: syncUUIDs,
}
default:
return nil, fmt.Errorf("unknown sync info value: %d", pkt.Identifier.Tag)
}
return c, nil
}
// GetControlType returns the OID
func (c *ControlSyncInfo) GetControlType() string {
return ControlTypeSyncInfo
}
// Encode encodes the control
func (c *ControlSyncInfo) Encode() *ber.Packet {
return nil
}
// String returns a human-readable description
func (c *ControlSyncInfo) String() string {
return fmt.Sprintf(
"Control Type: %s (%q) Criticality: %t Value: %d %s %s %s %s",
ControlTypeMap[ControlTypeSyncInfo],
ControlTypeSyncInfo,
c.Criticality,
c.Value,
c.NewCookie,
c.RefreshDelete,
c.RefreshPresent,
c.SyncIdSet,
)
}

View file

@ -1,13 +1,11 @@
package ldap
import (
"log"
ber "github.com/go-asn1-ber/asn1-ber"
)
// debugging type
// - has a Printf method to write the debug output
// - has a Printf method to write the debug output
type debugging bool
// Enable controls debugging mode.
@ -18,13 +16,13 @@ func (debug *debugging) Enable(b bool) {
// Printf writes debug output.
func (debug debugging) Printf(format string, args ...interface{}) {
if debug {
log.Printf(format, args...)
logger.Printf(format, args...)
}
}
// PrintPacket dumps a packet.
func (debug debugging) PrintPacket(packet *ber.Packet) {
if debug {
ber.WritePacket(log.Writer(), packet)
ber.WritePacket(logger.Writer(), packet)
}
}

View file

@ -1,8 +1,7 @@
package ldap
import (
"log"
"fmt"
ber "github.com/go-asn1-ber/asn1-ber"
)
@ -53,7 +52,8 @@ func (l *Conn) Del(delRequest *DelRequest) error {
return err
}
} else {
log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag)
}
return nil
}

View file

@ -5,6 +5,7 @@ import (
enchex "encoding/hex"
"errors"
"fmt"
"sort"
"strings"
ber "github.com/go-asn1-ber/asn1-ber"
@ -18,16 +19,95 @@ type AttributeTypeAndValue struct {
Value string
}
// String returns a normalized string representation of this attribute type and
// value pair which is the a lowercased join of the Type and Value with a "=".
func (a *AttributeTypeAndValue) String() string {
return strings.ToLower(a.Type) + "=" + a.encodeValue()
}
func (a *AttributeTypeAndValue) encodeValue() string {
// Normalize the value first.
// value := strings.ToLower(a.Value)
value := a.Value
encodedBuf := bytes.Buffer{}
escapeChar := func(c byte) {
encodedBuf.WriteByte('\\')
encodedBuf.WriteByte(c)
}
escapeHex := func(c byte) {
encodedBuf.WriteByte('\\')
encodedBuf.WriteString(enchex.EncodeToString([]byte{c}))
}
for i := 0; i < len(value); i++ {
char := value[i]
if i == 0 && char == ' ' || char == '#' {
// Special case leading space or number sign.
escapeChar(char)
continue
}
if i == len(value)-1 && char == ' ' {
// Special case trailing space.
escapeChar(char)
continue
}
switch char {
case '"', '+', ',', ';', '<', '>', '\\':
// Each of these special characters must be escaped.
escapeChar(char)
continue
}
if char < ' ' || char > '~' {
// All special character escapes are handled first
// above. All bytes less than ASCII SPACE and all bytes
// greater than ASCII TILDE must be hex-escaped.
escapeHex(char)
continue
}
// Any other character does not require escaping.
encodedBuf.WriteByte(char)
}
return encodedBuf.String()
}
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
type RelativeDN struct {
Attributes []*AttributeTypeAndValue
}
// String returns a normalized string representation of this relative DN which
// is the a join of all attributes (sorted in increasing order) with a "+".
func (r *RelativeDN) String() string {
attrs := make([]string, len(r.Attributes))
for i := range r.Attributes {
attrs[i] = r.Attributes[i].String()
}
sort.Strings(attrs)
return strings.Join(attrs, "+")
}
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
type DN struct {
RDNs []*RelativeDN
}
// String returns a normalized string representation of this DN which is the
// join of all relative DNs with a ",".
func (d *DN) String() string {
rdns := make([]string, len(d.RDNs))
for i := range d.RDNs {
rdns[i] = d.RDNs[i].String()
}
return strings.Join(rdns, ",")
}
// ParseDN returns a distinguishedName or an error.
// The function respects https://tools.ietf.org/html/rfc4514
func ParseDN(str string) (*DN, error) {
@ -76,7 +156,7 @@ func ParseDN(str string) (*DN, error) {
case char == '\\':
unescapedTrailingSpaces = 0
escaping = true
case char == '=':
case char == '=' && attribute.Type == "":
attribute.Type = stringFromBuffer()
// Special case: If the first character in the value is # the
// following data is BER encoded so we can just fast forward
@ -84,7 +164,7 @@ func ParseDN(str string) (*DN, error) {
if len(str) > i+1 && str[i+1] == '#' {
i += 2
index := strings.IndexAny(str[i:], ",+")
data := str
var data string
if index > 0 {
data = str[i : i+index]
} else {
@ -101,7 +181,7 @@ func ParseDN(str string) (*DN, error) {
buffer.WriteString(packet.Data.String())
i += len(data) - 1
}
case char == ',' || char == '+':
case char == ',' || char == '+' || char == ';':
// We're done with this RDN or value, push it
if len(attribute.Type) == 0 {
return nil, errors.New("incomplete type, value pair")
@ -109,7 +189,7 @@ func ParseDN(str string) (*DN, error) {
attribute.Value = stringFromBuffer()
rdn.Attributes = append(rdn.Attributes, attribute)
attribute = new(AttributeTypeAndValue)
if char == ',' {
if char == ',' || char == ';' {
dn.RDNs = append(dn.RDNs, rdn)
rdn = new(RelativeDN)
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
@ -206,7 +286,7 @@ func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
}
// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// EqualFold returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// Returns true if they have the same number of relative distinguished names
// and corresponding relative distinguished names (by position) are the same.
// Case of the attribute type and value is not significant
@ -238,7 +318,7 @@ func (d *DN) AncestorOfFold(other *DN) bool {
return true
}
// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// EqualFold returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// Case of the attribute type is not significant
func (r *RelativeDN) EqualFold(other *RelativeDN) bool {
if len(r.Attributes) != len(other.Attributes) {

View file

@ -192,6 +192,8 @@ func (e *Error) Error() string {
return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error())
}
func (e *Error) Unwrap() error { return e.Err }
// GetLDAPError creates an Error out of a BER packet representing a LDAPResult
// The return is an error object. It can be casted to a Error structure.
// This function returns nil if resultCode in the LDAPResult sequence is success(0).
@ -206,15 +208,21 @@ func GetLDAPError(packet *ber.Packet) error {
return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet}
}
if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
resultCode := uint16(response.Children[0].Value.(int64))
if resultCode == 0 { // No error
return nil
}
return &Error{
ResultCode: resultCode,
MatchedDN: response.Children[1].Value.(string),
Err: fmt.Errorf("%s", response.Children[2].Value.(string)),
Packet: packet,
if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) {
resultCode := uint16(response.Children[0].Value.(int64))
if resultCode == 0 { // No error
return nil
}
if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) &&
ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) {
return &Error{
ResultCode: resultCode,
MatchedDN: response.Children[1].Value.(string),
Err: fmt.Errorf("%s", response.Children[2].Value.(string)),
Packet: packet,
}
}
}
}
}

View file

@ -396,7 +396,7 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
case packet.Tag == FilterEqualityMatch && bytes.Equal(condition.Bytes(), _SymbolAny):
packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute.String(), FilterMap[FilterPresent])
case packet.Tag == FilterEqualityMatch && bytes.Index(condition.Bytes(), _SymbolAny) > -1:
case packet.Tag == FilterEqualityMatch && bytes.Contains(condition.Bytes(), _SymbolAny):
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute.String(), "Attribute"))
packet.Tag = FilterSubstrings
packet.Description = FilterMap[uint64(packet.Tag)]
@ -438,7 +438,6 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
// Convert from "ABC\xx\xx\xx" form to literal bytes for transport
func decodeEscapedSymbols(src []byte) (string, error) {
var (
buffer bytes.Buffer
offset int

View file

@ -3,7 +3,9 @@ package ldap
import (
"fmt"
"io/ioutil"
"log"
"os"
"strings"
ber "github.com/go-asn1-ber/asn1-ber"
)
@ -30,6 +32,7 @@ const (
ApplicationSearchResultReference = 19
ApplicationExtendedRequest = 23
ApplicationExtendedResponse = 24
ApplicationIntermediateResponse = 25
)
// ApplicationMap contains human readable descriptions of LDAP Application Codes
@ -54,6 +57,7 @@ var ApplicationMap = map[uint8]string{
ApplicationSearchResultReference: "Search Result Reference",
ApplicationExtendedRequest: "Extended Request",
ApplicationExtendedResponse: "Extended Response",
ApplicationIntermediateResponse: "Intermediate Response",
}
// Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
@ -82,6 +86,13 @@ var BeheraPasswordPolicyErrorMap = map[int8]string{
BeheraPasswordInHistory: "New password is in list of old passwords",
}
var logger = log.New(os.Stderr, "", log.LstdFlags)
// Logger allows clients to override the default logger
func Logger(l *log.Logger) {
logger = l
}
// Adds descriptions to an LDAP Response packet for debugging
func addLDAPDescriptions(packet *ber.Packet) (err error) {
defer func() {
@ -221,18 +232,18 @@ func addControlDescriptions(packet *ber.Packet) error {
sequence := value.Children[0]
for _, child := range sequence.Children {
if child.Tag == 0 {
//Warning
// Warning
warningPacket := child.Children[0]
val, err := ber.ParseInt64(warningPacket.Data.Bytes())
if err != nil {
return fmt.Errorf("failed to decode data bytes: %s", err)
}
if warningPacket.Tag == 0 {
//timeBeforeExpiration
// timeBeforeExpiration
value.Description += " (TimeBeforeExpiration)"
warningPacket.Value = val
} else if warningPacket.Tag == 1 {
//graceAuthNsRemaining
// graceAuthNsRemaining
value.Description += " (GraceAuthNsRemaining)"
warningPacket.Value = val
}
@ -337,3 +348,43 @@ func EscapeFilter(filter string) string {
}
return string(buf)
}
// EscapeDN escapes distinguished names as described in RFC4514. Characters in the
// set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing
// spaces or a leading `#`. Null bytes are replaced with `\00`.
func EscapeDN(dn string) string {
if dn == "" {
return ""
}
builder := strings.Builder{}
for i, r := range dn {
// Escape leading and trailing spaces
if (i == 0 || i == len(dn)-1) && r == ' ' {
builder.WriteRune('\\')
builder.WriteRune(r)
continue
}
// Escape leading '#'
if i == 0 && r == '#' {
builder.WriteRune('\\')
builder.WriteRune(r)
continue
}
// Escape characters as defined in RFC4514
switch r {
case '"', '+', ',', ';', '<', '>', '\\':
builder.WriteRune('\\')
builder.WriteRune(r)
case '\x00': // Null byte may not be escaped by a leading backslash
builder.WriteString("\\00")
default:
builder.WriteRune(r)
}
}
return builder.String()
}

View file

@ -1,8 +1,7 @@
package ldap
import (
"log"
"fmt"
ber "github.com/go-asn1-ber/asn1-ber"
)
@ -25,7 +24,9 @@ type ModifyDNRequest struct {
// RDN of the given DN.
//
// A call like
// mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "")
//
// mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "")
//
// will setup the request to just rename uid=someone,dc=example,dc=org to
// uid=newname,dc=example,dc=org.
func NewModifyDNRequest(dn string, rdn string, delOld bool, newSup string) *ModifyDNRequest {
@ -94,7 +95,8 @@ func (l *Conn) ModifyDN(m *ModifyDNRequest) error {
return err
}
} else {
log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag)
}
return nil
}

View file

@ -2,7 +2,7 @@ package ldap
import (
"errors"
"log"
"fmt"
ber "github.com/go-asn1-ber/asn1-ber"
)
@ -127,8 +127,9 @@ func (l *Conn) Modify(modifyRequest *ModifyRequest) error {
return err
}
} else {
log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag)
}
return nil
}
@ -136,6 +137,8 @@ func (l *Conn) Modify(modifyRequest *ModifyRequest) error {
type ModifyResult struct {
// Controls are the returned controls
Controls []Control
// Referral is the returned referral
Referral string
}
// ModifyWithResult performs the ModifyRequest and returns the result
@ -158,9 +161,10 @@ func (l *Conn) ModifyWithResult(modifyRequest *ModifyRequest) (*ModifyResult, er
switch packet.Children[1].Tag {
case ApplicationModifyResponse:
err := GetLDAPError(packet)
if err != nil {
return nil, err
if err = GetLDAPError(packet); err != nil {
result.Referral = getReferral(err, packet)
return result, err
}
if len(packet.Children) == 3 {
for _, child := range packet.Children[2].Children {

View file

@ -70,7 +70,6 @@ func (req *PasswordModifyRequest) appendTo(envelope *ber.Packet) error {
// newPassword is the desired user's password. If empty the server can return
// an error or generate a new password that will be available in the
// PasswordModifyResult.GeneratedPassword
//
func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest {
return &PasswordModifyRequest{
UserIdentity: userIdentity,
@ -95,15 +94,9 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa
result := &PasswordModifyResult{}
if packet.Children[1].Tag == ApplicationExtendedResponse {
err := GetLDAPError(packet)
if err != nil {
if IsErrorWithCode(err, LDAPResultReferral) {
for _, child := range packet.Children[1].Children {
if child.Tag == 3 {
result.Referral = child.Children[0].Value.(string)
}
}
}
if err = GetLDAPError(packet); err != nil {
result.Referral = getReferral(err, packet)
return result, err
}
} else {
@ -112,10 +105,10 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa
extendedResponse := packet.Children[1]
for _, child := range extendedResponse.Children {
if child.Tag == 11 {
if child.Tag == ber.TagEmbeddedPDV {
passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes())
if len(passwordModifyResponseValue.Children) == 1 {
if passwordModifyResponseValue.Children[0].Tag == 0 {
if passwordModifyResponseValue.Children[0].Tag == ber.TagEOC {
result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes())
}
}

View file

@ -9,7 +9,8 @@ import (
var (
errRespChanClosed = errors.New("ldap: response channel closed")
errCouldNotRetMsg = errors.New("ldap: could not retrieve message")
ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn")
// ErrNilConnection is returned if doRequest is called with a nil connection.
ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn")
)
type request interface {
@ -69,3 +70,41 @@ func (l *Conn) readPacket(msgCtx *messageContext) (*ber.Packet, error) {
}
return packet, nil
}
func getReferral(err error, packet *ber.Packet) (referral string) {
if !IsErrorWithCode(err, LDAPResultReferral) {
return ""
}
if len(packet.Children) < 2 {
return ""
}
// The packet Tag itself (of child 2) is generally a ber.TagObjectDescriptor with referrals however OpenLDAP
// seemingly returns a ber.Tag.GeneralizedTime. Every currently tested LDAP server which returns referrals returns
// an ASN.1 BER packet with the Type of ber.TypeConstructed and Class of ber.ClassApplication however. Thus this
// check expressly checks these fields instead.
//
// Related Issues:
// - https://github.com/authelia/authelia/issues/4199 (downstream)
if len(packet.Children[1].Children) == 0 || (packet.Children[1].TagType != ber.TypeConstructed || packet.Children[1].ClassType != ber.ClassApplication) {
return ""
}
var ok bool
for _, child := range packet.Children[1].Children {
// The referral URI itself should be contained within a child which has a Tag of ber.BitString or
// ber.TagPrintableString, and the Type of ber.TypeConstructed and the Class of ClassContext. As soon as any of
// these conditions is not true we can skip this child.
if (child.Tag != ber.TagBitString && child.Tag != ber.TagPrintableString) || child.TagType != ber.TypeConstructed || child.ClassType != ber.ClassContext {
continue
}
if referral, ok = child.Children[0].Value.(string); ok {
return referral
}
}
return ""
}

207
vendor/github.com/go-ldap/ldap/v3/response.go generated vendored Normal file
View file

@ -0,0 +1,207 @@
package ldap
import (
"context"
"errors"
"fmt"
ber "github.com/go-asn1-ber/asn1-ber"
)
// Response defines an interface to get data from an LDAP server
type Response interface {
Entry() *Entry
Referral() string
Controls() []Control
Err() error
Next() bool
}
type searchResponse struct {
conn *Conn
ch chan *SearchSingleResult
entry *Entry
referral string
controls []Control
err error
}
// Entry returns an entry from the given search request
func (r *searchResponse) Entry() *Entry {
return r.entry
}
// Referral returns a referral from the given search request
func (r *searchResponse) Referral() string {
return r.referral
}
// Controls returns controls from the given search request
func (r *searchResponse) Controls() []Control {
return r.controls
}
// Err returns an error when the given search request was failed
func (r *searchResponse) Err() error {
return r.err
}
// Next returns whether next data exist or not
func (r *searchResponse) Next() bool {
res, ok := <-r.ch
if !ok {
return false
}
if res == nil {
return false
}
r.err = res.Error
if r.err != nil {
return false
}
r.entry = res.Entry
r.referral = res.Referral
r.controls = res.Controls
return true
}
func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest) {
go func() {
defer func() {
close(r.ch)
if err := recover(); err != nil {
r.conn.err = fmt.Errorf("ldap: recovered panic in searchResponse: %v", err)
}
}()
if r.conn.IsClosing() {
return
}
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.conn.nextMessageID(), "MessageID"))
// encode search request
err := searchRequest.appendTo(packet)
if err != nil {
r.ch <- &SearchSingleResult{Error: err}
return
}
r.conn.Debug.PrintPacket(packet)
msgCtx, err := r.conn.sendMessage(packet)
if err != nil {
r.ch <- &SearchSingleResult{Error: err}
return
}
defer r.conn.finishMessage(msgCtx)
foundSearchSingleResultDone := false
for !foundSearchSingleResultDone {
select {
case <-ctx.Done():
r.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error())
return
default:
r.conn.Debug.Printf("%d: waiting for response", msgCtx.id)
packetResponse, ok := <-msgCtx.responses
if !ok {
err := NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
r.ch <- &SearchSingleResult{Error: err}
return
}
packet, err = packetResponse.ReadPacket()
r.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet)
if err != nil {
r.ch <- &SearchSingleResult{Error: err}
return
}
if r.conn.Debug {
if err := addLDAPDescriptions(packet); err != nil {
r.ch <- &SearchSingleResult{Error: err}
return
}
ber.PrintPacket(packet)
}
switch packet.Children[1].Tag {
case ApplicationSearchResultEntry:
result := &SearchSingleResult{
Entry: &Entry{
DN: packet.Children[1].Children[0].Value.(string),
Attributes: unpackAttributes(packet.Children[1].Children[1].Children),
},
}
if len(packet.Children) != 3 {
r.ch <- result
continue
}
decoded, err := DecodeControl(packet.Children[2].Children[0])
if err != nil {
werr := fmt.Errorf("failed to decode search result entry: %w", err)
result.Error = werr
r.ch <- result
return
}
result.Controls = append(result.Controls, decoded)
r.ch <- result
case ApplicationSearchResultDone:
if err := GetLDAPError(packet); err != nil {
r.ch <- &SearchSingleResult{Error: err}
return
}
if len(packet.Children) == 3 {
result := &SearchSingleResult{}
for _, child := range packet.Children[2].Children {
decodedChild, err := DecodeControl(child)
if err != nil {
werr := fmt.Errorf("failed to decode child control: %w", err)
r.ch <- &SearchSingleResult{Error: werr}
return
}
result.Controls = append(result.Controls, decodedChild)
}
r.ch <- result
}
foundSearchSingleResultDone = true
case ApplicationSearchResultReference:
ref := packet.Children[1].Children[0].Value.(string)
r.ch <- &SearchSingleResult{Referral: ref}
case ApplicationIntermediateResponse:
decoded, err := DecodeControl(packet.Children[1])
if err != nil {
werr := fmt.Errorf("failed to decode intermediate response: %w", err)
r.ch <- &SearchSingleResult{Error: werr}
return
}
result := &SearchSingleResult{}
result.Controls = append(result.Controls, decoded)
r.ch <- result
default:
err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag)
r.ch <- &SearchSingleResult{Error: err}
return
}
}
}
r.conn.Debug.Printf("%d: returning", msgCtx.id)
}()
}
func newSearchResponse(conn *Conn, bufferSize int) *searchResponse {
var ch chan *SearchSingleResult
if bufferSize > 0 {
ch = make(chan *SearchSingleResult, bufferSize)
} else {
ch = make(chan *SearchSingleResult)
}
return &searchResponse{
conn: conn,
ch: ch,
}
}

View file

@ -1,10 +1,14 @@
package ldap
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
ber "github.com/go-asn1-ber/asn1-ber"
)
@ -161,6 +165,155 @@ func (e *Entry) PrettyPrint(indent int) {
}
}
// Describe the tag to use for struct field tags
const decoderTagName = "ldap"
// readTag will read the reflect.StructField value for
// the key defined in decoderTagName. If omitempty is
// specified, the field may not be filled.
func readTag(f reflect.StructField) (string, bool) {
val, ok := f.Tag.Lookup(decoderTagName)
if !ok {
return f.Name, false
}
opts := strings.Split(val, ",")
omit := false
if len(opts) == 2 {
omit = opts[1] == "omitempty"
}
return opts[0], omit
}
// Unmarshal parses the Entry in the value pointed to by i
//
// Currently, this methods only supports struct fields of type
// string, []string, int, int64, []byte, *DN, []*DN or time.Time. Other field types
// will not be regarded. If the field type is a string or int but multiple
// attribute values are returned, the first value will be used to fill the field.
//
// Example:
//
// type UserEntry struct {
// // Fields with the tag key `dn` are automatically filled with the
// // objects distinguishedName. This can be used multiple times.
// DN string `ldap:"dn"`
//
// // This field will be filled with the attribute value for
// // userPrincipalName. An attribute can be read into a struct field
// // multiple times. Missing attributes will not result in an error.
// UserPrincipalName string `ldap:"userPrincipalName"`
//
// // memberOf may have multiple values. If you don't
// // know the amount of attribute values at runtime, use a string array.
// MemberOf []string `ldap:"memberOf"`
//
// // ID is an integer value, it will fail unmarshaling when the given
// // attribute value cannot be parsed into an integer.
// ID int `ldap:"id"`
//
// // LongID is similar to ID but uses an int64 instead.
// LongID int64 `ldap:"longId"`
//
// // Data is similar to MemberOf a slice containing all attribute
// // values.
// Data []byte `ldap:"data"`
//
// // Time is parsed with the generalizedTime spec into a time.Time
// Created time.Time `ldap:"createdTimestamp"`
//
// // *DN is parsed with the ParseDN
// Owner *ldap.DN `ldap:"owner"`
//
// // []*DN is parsed with the ParseDN
// Children []*ldap.DN `ldap:"children"`
//
// // This won't work, as the field is not of type string. For this
// // to work, you'll have to temporarily store the result in string
// // (or string array) and convert it to the desired type afterwards.
// UserAccountControl uint32 `ldap:"userPrincipalName"`
// }
// user := UserEntry{}
//
// if err := result.Unmarshal(&user); err != nil {
// // ...
// }
func (e *Entry) Unmarshal(i interface{}) (err error) {
// Make sure it's a ptr
if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr {
return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo)
}
sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem()
// Make sure it's pointing to a struct
if sv.Kind() != reflect.Struct {
return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind())
}
for n := 0; n < st.NumField(); n++ {
// Holds struct field value and type
fv, ft := sv.Field(n), st.Field(n)
// skip unexported fields
if ft.PkgPath != "" {
continue
}
// omitempty can be safely discarded, as it's not needed when unmarshalling
fieldTag, _ := readTag(ft)
// Fill the field with the distinguishedName if the tag key is `dn`
if fieldTag == "dn" {
fv.SetString(e.DN)
continue
}
values := e.GetAttributeValues(fieldTag)
if len(values) == 0 {
continue
}
switch fv.Interface().(type) {
case []string:
for _, item := range values {
fv.Set(reflect.Append(fv, reflect.ValueOf(item)))
}
case string:
fv.SetString(values[0])
case []byte:
fv.SetBytes([]byte(values[0]))
case int, int64:
intVal, err := strconv.ParseInt(values[0], 10, 64)
if err != nil {
return fmt.Errorf("ldap: could not parse value '%s' into int field", values[0])
}
fv.SetInt(intVal)
case time.Time:
t, err := ber.ParseGeneralizedTime([]byte(values[0]))
if err != nil {
return fmt.Errorf("ldap: could not parse value '%s' into time.Time field", values[0])
}
fv.Set(reflect.ValueOf(t))
case *DN:
dn, err := ParseDN(values[0])
if err != nil {
return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", values[0])
}
fv.Set(reflect.ValueOf(dn))
case []*DN:
for _, item := range values {
dn, err := ParseDN(item)
if err != nil {
return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", item)
}
fv.Set(reflect.Append(fv, reflect.ValueOf(dn)))
}
default:
return fmt.Errorf("ldap: expected field to be of type string, []string, int, int64, []byte, *DN, []*DN or time.Time, got %v", ft.Type)
}
}
return
}
// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair
func NewEntryAttribute(name string, values []string) *EntryAttribute {
var bytes [][]byte
@ -218,6 +371,35 @@ func (s *SearchResult) PrettyPrint(indent int) {
}
}
// appendTo appends all entries of `s` to `r`
func (s *SearchResult) appendTo(r *SearchResult) {
r.Entries = append(r.Entries, s.Entries...)
r.Referrals = append(r.Referrals, s.Referrals...)
r.Controls = append(r.Controls, s.Controls...)
}
// SearchSingleResult holds the server's single entry response to a search request
type SearchSingleResult struct {
// Entry is the returned entry
Entry *Entry
// Referral is the returned referral
Referral string
// Controls are the returned controls
Controls []Control
// Error is set when the search request was failed
Error error
}
// Print outputs a human-readable description
func (s *SearchSingleResult) Print() {
s.Entry.Print()
}
// PrettyPrint outputs a human-readable description with indenting
func (s *SearchSingleResult) PrettyPrint(indent int) {
s.Entry.PrettyPrint(indent)
}
// SearchRequest represents a search request to send to the server
type SearchRequest struct {
BaseDN string
@ -285,10 +467,11 @@ func NewSearchRequest(
// SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the
// search request. All paged LDAP query responses will be buffered and the final result will be returned atomically.
// The following four cases are possible given the arguments:
// - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size
// - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries
// - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request
// - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries
// - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size
// - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries
// - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request
// - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries
//
// A requested pagingSize of 0 is interpreted as no limit by LDAP servers.
func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) {
var pagingControl *ControlPaging
@ -311,23 +494,19 @@ func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32)
searchResult := new(SearchResult)
for {
result, err := l.Search(searchRequest)
l.Debug.Printf("Looking for Paging Control...")
if result != nil {
result.appendTo(searchResult)
} else {
if err == nil {
// We have to do this beautifulness in case something absolutely strange happens, which
// should only occur in case there is no packet, but also no error.
return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received"))
}
}
if err != nil {
// If an error occurred, all results that have been received so far will be returned
return searchResult, err
}
if result == nil {
return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received"))
}
for _, entry := range result.Entries {
searchResult.Entries = append(searchResult.Entries, entry)
}
for _, referral := range result.Referrals {
searchResult.Referrals = append(searchResult.Referrals, referral)
}
for _, control := range result.Controls {
searchResult.Controls = append(searchResult.Controls, control)
}
l.Debug.Printf("Looking for Paging Control...")
pagingResult := FindControl(result.Controls, ControlTypePaging)
@ -349,7 +528,9 @@ func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32)
if pagingControl != nil {
l.Debug.Printf("Abandoning Paging...")
pagingControl.PagingSize = 0
l.Search(searchRequest)
if _, err := l.Search(searchRequest); err != nil {
return searchResult, err
}
}
return searchResult, nil
@ -366,7 +547,8 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) {
result := &SearchResult{
Entries: make([]*Entry, 0),
Referrals: make([]string, 0),
Controls: make([]Control, 0)}
Controls: make([]Control, 0),
}
for {
packet, err := l.readPacket(msgCtx)
@ -402,6 +584,32 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) {
}
}
// SearchAsync performs a search request and returns all search results asynchronously.
// This means you get all results until an error happens (or the search successfully finished),
// e.g. for size / time limited requests all are recieved until the limit is reached.
// To stop the search, call cancel function of the context.
func (l *Conn) SearchAsync(
ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response {
r := newSearchResponse(l, bufferSize)
r.start(ctx, searchRequest)
return r
}
// Syncrepl is a short name for LDAP Sync Replication engine that works on the
// consumer-side. This can perform a persistent search and returns an entry
// when the entry is updated on the server side.
// To stop the search, call cancel function of the context.
func (l *Conn) Syncrepl(
ctx context.Context, searchRequest *SearchRequest, bufferSize int,
mode ControlSyncRequestMode, cookie []byte, reloadHint bool,
) Response {
control := NewControlSyncRequest(mode, cookie, reloadHint)
searchRequest.Controls = append(searchRequest.Controls, control)
r := newSearchResponse(l, bufferSize)
r.start(ctx, searchRequest)
return r
}
// unpackAttributes will extract all given LDAP attributes and it's values
// from the ber.Packet
func unpackAttributes(children []*ber.Packet) []*EntryAttribute {
@ -425,3 +633,58 @@ func unpackAttributes(children []*ber.Packet) []*EntryAttribute {
return entries
}
// DirSync does a Search with dirSync Control.
func (l *Conn) DirSync(
searchRequest *SearchRequest, flags int64, maxAttrCount int64, cookie []byte,
) (*SearchResult, error) {
control := FindControl(searchRequest.Controls, ControlTypeDirSync)
if control == nil {
c := NewRequestControlDirSync(flags, maxAttrCount, cookie)
searchRequest.Controls = append(searchRequest.Controls, c)
} else {
c := control.(*ControlDirSync)
if c.Flags != flags {
return nil, fmt.Errorf("flags given in search request (%d) conflicts with flags given in search call (%d)", c.Flags, flags)
}
if c.MaxAttrCount != maxAttrCount {
return nil, fmt.Errorf("MaxAttrCnt given in search request (%d) conflicts with maxAttrCount given in search call (%d)", c.MaxAttrCount, maxAttrCount)
}
}
searchResult, err := l.Search(searchRequest)
l.Debug.Printf("Looking for result...")
if err != nil {
return nil, err
}
if searchResult == nil {
return nil, NewError(ErrorNetwork, errors.New("ldap: packet not received"))
}
l.Debug.Printf("Looking for DirSync Control...")
resultControl := FindControl(searchResult.Controls, ControlTypeDirSync)
if resultControl == nil {
l.Debug.Printf("Could not find dirSyncControl control. Breaking...")
return searchResult, nil
}
cookie = resultControl.(*ControlDirSync).Cookie
if len(cookie) == 0 {
l.Debug.Printf("Could not find cookie. Breaking...")
return searchResult, nil
}
return searchResult, nil
}
// DirSyncDirSyncAsync performs a search request and returns all search results
// asynchronously. This is efficient when the server returns lots of entries.
func (l *Conn) DirSyncAsync(
ctx context.Context, searchRequest *SearchRequest, bufferSize int,
flags, maxAttrCount int64, cookie []byte,
) Response {
control := NewRequestControlDirSync(flags, maxAttrCount, cookie)
searchRequest.Controls = append(searchRequest.Controls, control)
r := newSearchResponse(l, bufferSize)
r.start(ctx, searchRequest)
return r
}

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