redmine

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 22, 2024 License: MIT Imports: 10 Imported by: 0

README

Redmine REST API client

codecov Go Reference goreportcard

This is a lightweight Redmine API client.

Introduction

[!CAUTION] I created it for my own personal use (while I was learning the Go language), you probably shouldn't use it! At least until it ready for production: ver >= 1.0.

I wanted create something like TUI of Redmine, but when i dive into learning of TUI libs like bubbletea, i spent all my free time slots and this idea was on pause for a while...until once i needed small lib to get all my time entries from redmine to build over this one some another cool TUI app.

So basically this lib, a Go module, used for scroll over all Redmine time entries.

Another functional will be added (maybe) later...

Installation

go get github.com/1buran/redmine

Usage

Supported Redmine types:

  • User
  • Project
  • TimeEntry
  • Issue
// Configure the time entries filter: set start and end date of time frame and user id
// for whom you want to get time entries
start := time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 0, 30)

timeEntriesFilter := redmine.TimeEntriesFilter{
    StartDate: start,
    EndDate: end,
    UserId: "1"
}

// Create an API client: set Redmine REST API url, token, enable or disable logging
apiClient := redmine.CreateApiClient(
    "https://example.com", "asdadwwefwefwf", true, timeEntriesFilter)

// Open the channels to data and errors from the redmine client:
// dataChan, errChan := redmine.Scroll[redmine.Projects](apiClient)
// dataChan, errChan := redmine.Scroll[redmine.Issues](apiClient)
dataChan, errChan := redmine.Scroll[redmine.TimeEntries](apiClient)
for {
    select {
    case data, ok := <-dataChan:
        if ok { // data channel is open
            // perform action on the gotten items e.g. print the data
            for _, t := range data.Items {
                fmt.Printf("On %s user spent %.2f hours for %s\n", t.SpentOn, t.Hours, t.Comment)
            }
            continue
        }
        return // data channel is closed, all data is transmitted, return to the main loop
    case err, ok := <-errChan:
        if ok { // err channel is open
            // perform action depending on the gotten error and your business logic
            switch {
                case errors.Is(err, redmine.ApiEndpointUrlFatalError):
                    fmt.Fatalf("redmine api url is malformed: %s", err)
                case errors.Is(err, redmine.HttpError):
                    fmt.Println("http error: %s, retry...", err)
                default:
                    fmt.Println("err: %s", err)
            }
        }
    }
}

There are some custom error types, from low level to high level errors which are aggregates of first ones. Typically you should be expect only these high level errors in errChan:

  • JsonDecodeError: errors related to unmarshaling redmine server response
  • IoReadError: errors related to read input (io.ReadAll(body))
  • HttpError: errors related to network layer (http_client.Do(req))
  • ApiEndpointUrlFatalError: fatal errors that means that most probably the url of redmine api is malformed or bogus, please check it
  • ApiNewRequestFatalError: actually will not be thrown (see the comments in code)

Documentation

Overview

This is a lightweight Redmine API client.

It doesn't do a lot of things, you might probably only be interested in the scrolling feature Scroll.

Index

Constants

View Source
const (
	ProjectsApiEndpoint = "/projects.json"
	IssuesApiEndpoint   = "/issues.json"
	TimeEntriesEndpoint = "/time_entries.json"
)

Variables

View Source
var (
	JsonDecodeError          = errors.New("JSON decode error")
	IoReadError              = errors.New("io.ReadAll error")
	UrlJoinPathError         = errors.New("url.JoinPath error")
	UrlParseError            = errors.New("url.Parse error")
	ApiEndpointUrlFatalError = errors.New("cannot build API endpoint url")
	ApiNewRequestFatalError  = errors.New("cannot create a new request with given url")
	HttpError                = errors.New("http error")
	UnknownDataTypeError     = errors.New("unknown or not supported data type requested")
	ValidationError          = errors.New("Validation error")
	ZeroTimeDetectedError    = errors.New("Zero timestamp is not allowed")

	ProjectAndIssuePassedError = errors.New("Only one is required: issue_id or project_id")
	ProjectAndIssueMissedError = errors.New("Neither issue_id or project_id was passed")

	EmptyProjectError = errors.New("Project ID must not be a zero")
)

There are some custom error types, from low level to high level errors which are aggregates of first ones.

Typically you should be expect only these high level errors in errChan:

Functions

func ApiUrl added in v0.1.0

func ApiUrl[E Entities](ac *ApiClient, page int) (string, error)

func BuildApiUrl

func BuildApiUrl(base, endpoint string, v *url.Values, p int) (string, error)

Add pagination query string to URL.

func Create added in v0.2.0

func Create[P PostData](ac *ApiClient, data P) error

Create a redmine entity.

func DecodeResp

func DecodeResp[E Entities](body io.ReadCloser) (*E, error)

Decode JSON Redmine API response to package types.

func Scroll

func Scroll[E Entities](ac *ApiClient) (<-chan E, <-chan error)

Scroll over Redmine API paginated responses. It going through all available data, so it may generate a lot of http requests (depending on a size of data and pagination limit).

The pagination of redmine is based on offset&limit, but in URL you may use query string param ?page=, e.g. for 53 issues and limit=25 it will be three requests:

  • 0 25 53 - [0, 25] /issues.json?page=1 or omitted page number: /issues.json
  • 25 25 53 - [25, 50] /issues.json?page=2
  • 50 25 53 - [50, 53] /issues.json?page=3

This function do this automatically and send all the data to channel, if any error occurs, it will be send to the second, errors channel.

Types

type ApiClient added in v0.1.0

type ApiClient struct {
	Url        string
	Token      string
	LogEnabled bool
	TimeEntriesFilter
}

Config of Redmine REST API client: url, token, logging and time entries filtration.

func CreateApiClient added in v0.1.0

func CreateApiClient(url, token string, logging bool, teFilter TimeEntriesFilter) *ApiClient

func (ApiClient) Create added in v0.2.0

func (ac ApiClient) Create(url string, data io.Reader) error

Create entity

func (ApiClient) Get added in v0.1.0

func (ac ApiClient) Get(url string) (io.ReadCloser, error)

Get Redmine entities respecting the setted filtration (time entries) and page of pagination.

func (ApiClient) IssuesUrl added in v0.1.0

func (ac ApiClient) IssuesUrl(page int) (string, error)

func (ApiClient) Post added in v0.2.0

func (ac ApiClient) Post(url string, data io.Reader) (int, io.ReadCloser, error)

Post Redmine entity

func (ApiClient) ProjectsUrl added in v0.1.0

func (ac ApiClient) ProjectsUrl(page int) (string, error)

func (ApiClient) TimeEntriesUrl added in v0.1.0

func (ac ApiClient) TimeEntriesUrl(page int) (string, error)

type CreateIssuePayload added in v0.2.0

type CreateIssuePayload struct {
	ProjectID  int     `json:"project_id,omitempty"`
	TrackerID  int     `json:"tracker_id,omitempty"`
	StatusID   int     `json:"status_id,omitempty"`
	PriorityID int     `json:"priority_id,omitempty"`
	CategoryID int     `json:"category_id,omitempty"`
	ParrentID  int     `json:"parent_issue_id,omitempty"`
	FixedVerID int     `json:"fixed_version_id,omitempty"`
	AssignedID int     `json:"assigned_to_id,omitempty"`
	Watchers   []int   `json:"watcher_user_ids,omitempty"`
	Subject    string  `json:"string,omitempty"`
	Desc       string  `json:"description,omitempty"`
	Private    bool    `json:"is_private,omitempty"`
	Estimate   float32 `json:"estimated_hours,omitempty"`
}

Payload of Redmine API POST /issues

func (CreateIssuePayload) Validate added in v0.2.0

func (p CreateIssuePayload) Validate() error

Validate payload.

type CreateTimeEntryPayload added in v0.2.0

type CreateTimeEntryPayload struct {
	ProjectID  int     `json:"project_id,omitempty"`
	IssueID    int     `json:"issue_id,omitempty"`
	ActivityID int     `json:"activity_id,omitempty"`
	UserID     int     `json:"user_id,omitempty"`
	SpentOn    Date    `json:"spent_on,omitempty"`
	Comments   string  `json:"comments,omitempty"`
	Hours      float32 `json:"hours,omitempty"`
}

Payload of Redmine API POST /time_entries.

func (CreateTimeEntryPayload) Validate added in v0.2.0

func (p CreateTimeEntryPayload) Validate() error

Validate payload.

type Date

type Date struct {
	time.Time
}

A date type is needed for proper parsing (unmarshaling) of redmine date format used in JSON.

func (Date) MarshalJSON added in v0.2.0

func (d Date) MarshalJSON() ([]byte, error)

Marshaling time.Time object to redmine format.

func (Date) String

func (d Date) String() string

func (*Date) UnmarshalJSON

func (d *Date) UnmarshalJSON(b []byte) error

Unmarshaling redmine dates.

type Entities

type Entities interface {
	Projects | Issues | TimeEntries

	NextPage() (n int)
}

Data type constraint, a quick glance at which will let you know the supported data types for fetching from redmine server.

type Issue

type Issue struct {
	Id      int    `json:"id"`
	Subject string `json:"subject"`
	Desc    string `json:"description"`
	Project `json:"project"`
}

A Redmine issue entity.

func (Issue) String

func (i Issue) String() string

type Issues added in v0.1.0

type Issues struct {
	Items []Issue `json:"issues"`
	Pagination
}

type Pagination

type Pagination struct {
	Offset int `json:"offset"`
	Limit  int `json:"limit"`
	Total  int `json:"total_count"`
}

func (Pagination) NextPage added in v0.1.0

func (p Pagination) NextPage() (n int)

type PostData added in v0.2.0

type PostData interface {
	PostTimeEntryParams | PostDataIssue

	Validate() error
	Url(base string) (string, error)
}

PostData is a generic container for payloads of API endpoints.

type PostDataIssue added in v0.2.0

type PostDataIssue struct {
	Payload CreateIssuePayload `json:"issue"`
}

POST /issues params

func NewPostIssueParams added in v0.2.0

func NewPostIssueParams() *PostDataIssue

func (PostDataIssue) Url added in v0.2.0

func (i PostDataIssue) Url(base string) (string, error)

func (PostDataIssue) Validate added in v0.2.0

func (i PostDataIssue) Validate() error

type PostTimeEntryParams added in v0.2.0

type PostTimeEntryParams struct {
	Payload CreateTimeEntryPayload `json:"time_entry"`
}

POST /time_entries params

func NewPostTimeEntryParams added in v0.2.0

func NewPostTimeEntryParams() *PostTimeEntryParams

func (PostTimeEntryParams) Url added in v0.2.0

func (t PostTimeEntryParams) Url(base string) (string, error)

func (PostTimeEntryParams) Validate added in v0.2.0

func (t PostTimeEntryParams) Validate() error

type Project

type Project struct {
	Id    int    `json:"id"`
	Name  string `json:"name"`
	Ident string `json:"identifier"`
	Desc  string `json:"description"`
	// TODO correct parsing date time
	// CreatedOn time.Time `json:"created_on"`
	// UpdatedOn time.Time `json:"updated_on"`
	IsPublic bool `json:"is_public"`
}

A Redmine project entity.

type Projects added in v0.1.0

type Projects struct {
	Items []Project `json:"projects"`
	Pagination
}

type TimeEntries added in v0.1.0

type TimeEntries struct {
	Items []TimeEntry `json:"time_entries"`
	Pagination
}

type TimeEntriesFilter

type TimeEntriesFilter struct {
	StartDate time.Time
	EndDate   time.Time
	UserId    string
}

Time Entries filtration by range of dates and user id.

type TimeEntry

type TimeEntry struct {
	Id      int `json:"id"`
	Project `json:"project"`
	Issue   `json:"issue"`
	User    `json:"user"`
	Hours   float32 `json:"hours"`
	Comment string  `json:"comments"`
	SpentOn Date    `json:"spent_on"`
}

A Redmine time entries.

func (TimeEntry) String

func (t TimeEntry) String() string

type User

type User struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
}

A Redmine user entity.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL