func SearchTag(tag string) SearchParam {
	return func(v *url.Values) {
		if tag != "" {
			v.Add("tag", tag)
		}
	}
}
SearchTag specifies Search tag to search for.
Empty tag is silently ignored.
Can be specified multiple times, logical OR is applied.
package sdk
import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"
	"github.com/pkg/errors"
)
const DefaultFolderId = 0
type BoardProperties struct {
	IsStarred   bool      `json:"isStarred,omitempty"`
	IsHome      bool      `json:"isHome,omitempty"`
	IsSnapshot  bool      `json:"isSnapshot,omitempty"`
	Type        string    `json:"type,omitempty"`
	CanSave     bool      `json:"canSave"`
	CanEdit     bool      `json:"canEdit"`
	CanStar     bool      `json:"canStar"`
	Slug        string    `json:"slug"`
	Expires     time.Time `json:"expires"`
	Created     time.Time `json:"created"`
	Updated     time.Time `json:"updated"`
	UpdatedBy   string    `json:"updatedBy"`
	CreatedBy   string    `json:"createdBy"`
	Version     int       `json:"version"`
	FolderID    int       `json:"folderId"`
	FolderTitle string    `json:"folderTitle"`
	FolderURL   string    `json:"folderUrl"`
}
func (r *Client) GetDashboardByUID(ctx context.Context, uid string) (Board, BoardProperties, error) {
	return r.getDashboard(ctx, "uid/"+uid)
}
func (r *Client) GetDashboardBySlug(ctx context.Context, slug string) (Board, BoardProperties, error) {
	path := setPrefix(slug)
	return r.getDashboard(ctx, path)
}
func (r *Client) getDashboard(ctx context.Context, path string) (Board, BoardProperties, error) {
	raw, bp, err := r.getRawDashboard(ctx, path)
	if err != nil {
		return Board{}, BoardProperties{}, errors.Wrap(err, "get raw dashboard")
	}
	var (
		result struct {
			Meta  BoardProperties `json:"meta"`
			Board Board           `json:"dashboard"`
		}
	)
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.UseNumber()
	if err := dec.Decode(&result.Board); err != nil {
		return Board{}, BoardProperties{}, errors.Wrap(err, "unmarshal board")
	}
	return result.Board, bp, err
}
func (r *Client) getRawDashboard(ctx context.Context, path string) ([]byte, BoardProperties, error) {
	var (
		raw    []byte
		result struct {
			Meta  BoardProperties `json:"meta"`
			Board json.RawMessage `json:"dashboard"`
		}
		code int
		err  error
	)
	if raw, code, err = r.get(ctx, fmt.Sprintf("api/dashboards/%s", path), nil); err != nil {
		return nil, BoardProperties{}, err
	}
	if code != 200 {
		return nil, BoardProperties{}, fmt.Errorf("HTTP error %d: returns %s", code, raw)
	}
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.UseNumber()
	if err := dec.Decode(&result); err != nil {
		return nil, BoardProperties{}, errors.Wrap(err, "unmarshal board")
	}
	return []byte(result.Board), result.Meta, err
}
func (r *Client) GetRawDashboardByUID(ctx context.Context, uid string) ([]byte, BoardProperties, error) {
	return r.getRawDashboard(ctx, "uid/"+uid)
}
func (r *Client) GetRawDashboardBySlug(ctx context.Context, slug string) ([]byte, BoardProperties, error) {
	path := setPrefix(slug)
	return r.getRawDashboard(ctx, path)
}
type FoundBoard struct {
	ID          uint     `json:"id"`
	UID         string   `json:"uid"`
	Title       string   `json:"title"`
	URI         string   `json:"uri"`
	URL         string   `json:"url"`
	Slug        string   `json:"slug"`
	Type        string   `json:"type"`
	Tags        []string `json:"tags"`
	IsStarred   bool     `json:"isStarred"`
	FolderID    int      `json:"folderId"`
	FolderUID   string   `json:"folderUid"`
	FolderTitle string   `json:"folderTitle"`
	FolderURL   string   `json:"folderUrl"`
}
func (r *Client) SearchDashboards(ctx context.Context, query string, starred bool, tags ...string) ([]FoundBoard, error) {
	params := []SearchParam{
		SearchType(SearchTypeDashboard),
		SearchQuery(query),
		SearchStarred(starred),
	}
	for _, tag := range tags {
		params = append(params, SearchTag(tag))
	}
	return r.Search(ctx, params...)
}
func (r *Client) Search(ctx context.Context, params ...SearchParam) ([]FoundBoard, error) {
	var (
		raw    []byte
		boards []FoundBoard
		code   int
		err    error
	)
	u := url.URL{}
	q := u.Query()
	for _, p := range params {
		p(&q)
	}
	if raw, code, err = r.get(ctx, "api/search", q); err != nil {
		return nil, err
	}
	if code != 200 {
		return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
	}
	err = json.Unmarshal(raw, &boards)
	return boards, err
}
type SetDashboardParams struct {
	FolderID  int
	Overwrite bool
}
func (r *Client) SetDashboard(ctx context.Context, board Board, params SetDashboardParams) (StatusMessage, error) {
	var (
		isBoardFromDB bool
		newBoard      struct {
			Dashboard Board `json:"dashboard"`
			FolderID  int   `json:"folderId"`
			Overwrite bool  `json:"overwrite"`
		}
		raw  []byte
		resp StatusMessage
		code int
		err  error
	)
	if board.Slug, isBoardFromDB = cleanPrefix(board.Slug); !isBoardFromDB {
		return StatusMessage{}, errors.New("only database dashboard (with 'db/' prefix in a slug) can be set")
	}
	newBoard.Dashboard = board
	newBoard.FolderID = params.FolderID
	newBoard.Overwrite = params.Overwrite
	if !params.Overwrite {
		newBoard.Dashboard.ID = 0
	}
	if raw, err = json.Marshal(newBoard); err != nil {
		return StatusMessage{}, err
	}
	if raw, code, err = r.post(ctx, "api/dashboards/db", nil, raw); err != nil {
		return StatusMessage{}, err
	}
	if err = json.Unmarshal(raw, &resp); err != nil {
		return StatusMessage{}, err
	}
	if code != 200 {
		return resp, fmt.Errorf("HTTP error %d: returns %s", code, *resp.Message)
	}
	return resp, nil
}
func (r *Client) SetRawDashboard(ctx context.Context, raw []byte) (StatusMessage, error) {
	var (
		rawResp []byte
		resp    StatusMessage
		code    int
		err     error
		buf     bytes.Buffer
		plain   = make(map[string]interface{})
	)
	if err = json.Unmarshal(raw, &plain); err != nil {
		return StatusMessage{}, err
	}
	plain["id"] = 0
	raw, _ = json.Marshal(plain)
	buf.WriteString(`{"dashboard":`)
	buf.Write(raw)
	buf.WriteString(`, "overwrite": true}`)
	if rawResp, code, err = r.post(ctx, "api/dashboards/db", nil, buf.Bytes()); err != nil {
		return StatusMessage{}, err
	}
	if err = json.Unmarshal(rawResp, &resp); err != nil {
		return StatusMessage{}, err
	}
	if code != 200 {
		return StatusMessage{}, fmt.Errorf("HTTP error %d: returns %s", code, *resp.Message)
	}
	return resp, nil
}
func (r *Client) DeleteDashboard(ctx context.Context, slug string) (StatusMessage, error) {
	var (
		isBoardFromDB bool
		raw           []byte
		reply         StatusMessage
		err           error
	)
	if slug, isBoardFromDB = cleanPrefix(slug); !isBoardFromDB {
		return StatusMessage{}, errors.New("only database dashboards (with 'db/' prefix in a slug) can be removed")
	}
	if raw, _, err = r.delete(ctx, fmt.Sprintf("api/dashboards/db/%s", slug)); err != nil {
		return StatusMessage{}, err
	}
	err = json.Unmarshal(raw, &reply)
	return reply, err
}
type (
	SearchParam func(*url.Values)
	SearchParamType string
)
const (
	SearchTypeFolder    SearchParamType = "dash-folder"
	SearchTypeDashboard SearchParamType = "dash-db"
)
func SearchQuery(query string) SearchParam {
	return func(v *url.Values) {
		if query != "" {
			v.Set("query", query)
		}
	}
}