diff --git a/jira/internal/jql_impl.go b/jira/internal/jql_impl.go new file mode 100644 index 00000000..9f20462a --- /dev/null +++ b/jira/internal/jql_impl.go @@ -0,0 +1,80 @@ +package internal + +import ( + "context" + "fmt" + model "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "github.com/ctreminiom/go-atlassian/service" + "github.com/ctreminiom/go-atlassian/service/jira" + "net/http" + "net/url" + "strings" +) + +func NewJQLService(client service.Client, version string) (*JQLService, error) { + + if version == "" { + return nil, model.ErrNoVersionProvided + } + + return &JQLService{ + internalClient: &internalJQLServiceImpl{c: client, version: version}, + }, nil +} + +type JQLService struct { + internalClient jira.JQLConnector +} + +// Parse parses and validates JQL queries. +// +// Validation is performed in context of the current user. +// +// POST /rest/api/{2-3}/jql/parse +// +// https://docs.go-atlassian.io/jira-software-cloud/jql#parse-jql-query +func (j *JQLService) Parse(ctx context.Context, validationType string, JqlQueries []string) (*model.ParsedQueryPageScheme, *model.ResponseScheme, error) { + return j.internalClient.Parse(ctx, validationType, JqlQueries) +} + +type internalJQLServiceImpl struct { + c service.Client + version string +} + +func (i *internalJQLServiceImpl) Parse(ctx context.Context, validationType string, JqlQueries []string) (*model.ParsedQueryPageScheme, *model.ResponseScheme, error) { + + var endpoint strings.Builder + endpoint.WriteString(fmt.Sprintf("/rest/api/%v/jql/parse", i.version)) + + if validationType != "" { + params := url.Values{} + params.Add("validation", validationType) + + endpoint.WriteString(fmt.Sprintf("?%v", params.Encode())) + } + + payload := struct { + Queries []string `json:"queries,omitempty"` + }{ + Queries: JqlQueries, + } + + reader, err := i.c.TransformStructToReader(&payload) + if err != nil { + return nil, nil, err + } + + request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint.String(), reader) + if err != nil { + return nil, nil, err + } + + page := new(model.ParsedQueryPageScheme) + response, err := i.c.Call(request, page) + if err != nil { + return nil, response, err + } + + return page, response, nil +} diff --git a/jira/internal/jql_impl_test.go b/jira/internal/jql_impl_test.go new file mode 100644 index 00000000..e3fe501e --- /dev/null +++ b/jira/internal/jql_impl_test.go @@ -0,0 +1,189 @@ +package internal + +import ( + "bytes" + "context" + "errors" + model "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "github.com/ctreminiom/go-atlassian/service" + "github.com/ctreminiom/go-atlassian/service/mocks" + "github.com/stretchr/testify/assert" + "net/http" + "testing" +) + +func Test_internalJQLServiceImpl_Parse(t *testing.T) { + + payloadMocked := &struct { + Queries []string "json:\"queries,omitempty\"" + }{Queries: []string{"summary ~ test AND (labels in (urgent, blocker) OR lastCommentedBy = currentUser()) AND status CHANGED AFTER startOfMonth(-1M) ORDER BY updated DESC", "invalid query", "summary = test", "summary in test", "project = INVALID", "universe = 42"}} + + type fields struct { + c service.Client + version string + } + + type args struct { + ctx context.Context + validationType string + JqlQueries []string + } + + testCases := []struct { + name string + fields fields + args args + on func(*fields) + wantErr bool + Err error + }{ + { + name: "when the api version is v3", + fields: fields{version: "3"}, + args: args{ + ctx: context.TODO(), + validationType: "strict", + JqlQueries: []string{ + "summary ~ test AND (labels in (urgent, blocker) OR lastCommentedBy = currentUser()) AND status CHANGED AFTER startOfMonth(-1M) ORDER BY updated DESC", + "invalid query", + "summary = test", + "summary in test", + "project = INVALID", + "universe = 42"}, + }, + on: func(fields *fields) { + + client := mocks.NewClient(t) + + client.On("TransformStructToReader", + payloadMocked). + Return(bytes.NewReader([]byte{}), nil) + + client.On("NewRequest", + context.Background(), + http.MethodPost, + "/rest/api/3/jql/parse?validation=strict", + bytes.NewReader([]byte{})). + Return(&http.Request{}, nil) + + client.On("Call", + &http.Request{}, + &model.ParsedQueryPageScheme{}). + Return(&model.ResponseScheme{}, nil) + + fields.c = client + + }, + wantErr: false, + Err: nil, + }, + + { + name: "when the api version is v2", + fields: fields{version: "2"}, + args: args{ + ctx: context.TODO(), + validationType: "strict", + JqlQueries: []string{ + "summary ~ test AND (labels in (urgent, blocker) OR lastCommentedBy = currentUser()) AND status CHANGED AFTER startOfMonth(-1M) ORDER BY updated DESC", + "invalid query", + "summary = test", + "summary in test", + "project = INVALID", + "universe = 42"}, + }, + on: func(fields *fields) { + + client := mocks.NewClient(t) + + client.On("TransformStructToReader", + payloadMocked). + Return(bytes.NewReader([]byte{}), nil) + + client.On("NewRequest", + context.Background(), + http.MethodPost, + "/rest/api/2/jql/parse?validation=strict", + bytes.NewReader([]byte{})). + Return(&http.Request{}, nil) + + client.On("Call", + &http.Request{}, + &model.ParsedQueryPageScheme{}). + Return(&model.ResponseScheme{}, nil) + + fields.c = client + + }, + wantErr: false, + Err: nil, + }, + + { + name: "when the http request cannot be created", + fields: fields{version: "3"}, + args: args{ + ctx: context.TODO(), + validationType: "strict", + JqlQueries: []string{ + "summary ~ test AND (labels in (urgent, blocker) OR lastCommentedBy = currentUser()) AND status CHANGED AFTER startOfMonth(-1M) ORDER BY updated DESC", + "invalid query", + "summary = test", + "summary in test", + "project = INVALID", + "universe = 42"}, + }, + on: func(fields *fields) { + + client := mocks.NewClient(t) + + client.On("TransformStructToReader", + payloadMocked). + Return(bytes.NewReader([]byte{}), nil) + + client.On("NewRequest", + context.Background(), + http.MethodPost, + "/rest/api/3/jql/parse?validation=strict", + bytes.NewReader([]byte{})). + Return(&http.Request{}, errors.New("error, unable to create the http request")) + + fields.c = client + + }, + wantErr: true, + Err: errors.New("error, unable to create the http request"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + if testCase.on != nil { + testCase.on(&testCase.fields) + } + + fieldService, err := NewJQLService(testCase.fields.c, testCase.fields.version) + assert.NoError(t, err) + + gotResult, gotResponse, err := fieldService.Parse(testCase.args.ctx, testCase.args.validationType, + testCase.args.JqlQueries) + + if testCase.wantErr { + + if err != nil { + t.Logf("error returned: %v", err.Error()) + } + + assert.EqualError(t, err, testCase.Err.Error()) + + } else { + + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + } + + }) + } +} diff --git a/jira/v2/api_client_impl.go b/jira/v2/api_client_impl.go index 43489f12..10553385 100644 --- a/jira/v2/api_client_impl.go +++ b/jira/v2/api_client_impl.go @@ -333,6 +333,11 @@ func New(httpClient common.HttpClient, site string) (*Client, error) { return nil, err } + jql, err := internal.NewJQLService(client, "2") + if err != nil { + return nil, err + } + client.Permission = permission client.MySelf = mySelf client.Auth = internal.NewAuthenticationService(client) @@ -347,6 +352,7 @@ func New(httpClient common.HttpClient, site string) (*Client, error) { client.Task = task client.User = user client.Workflow = workflow + client.JQL = jql return client, nil } @@ -368,6 +374,7 @@ type Client struct { Server *internal.ServerService User *internal.UserService Workflow *internal.WorkflowService + JQL *internal.JQLService } func (c *Client) NewFormRequest(ctx context.Context, method, apiEndpoint, contentType string, payload io.Reader) (*http.Request, error) { diff --git a/jira/v3/api_client_impl.go b/jira/v3/api_client_impl.go index 7b1ccb3a..19911664 100644 --- a/jira/v3/api_client_impl.go +++ b/jira/v3/api_client_impl.go @@ -333,6 +333,11 @@ func New(httpClient common.HttpClient, site string) (*Client, error) { return nil, err } + jql, err := internal.NewJQLService(client, "3") + if err != nil { + return nil, err + } + client.Permission = permission client.MySelf = mySelf client.Auth = internal.NewAuthenticationService(client) @@ -347,6 +352,7 @@ func New(httpClient common.HttpClient, site string) (*Client, error) { client.Server = server client.User = user client.Workflow = workflow + client.JQL = jql return client, nil } @@ -368,6 +374,7 @@ type Client struct { Server *internal.ServerService User *internal.UserService Workflow *internal.WorkflowService + JQL *internal.JQLService } func (c *Client) NewFormRequest(ctx context.Context, method, apiEndpoint, contentType string, payload io.Reader) (*http.Request, error) { diff --git a/pkg/infra/models/jira_jql.go b/pkg/infra/models/jira_jql.go new file mode 100644 index 00000000..fc13aa10 --- /dev/null +++ b/pkg/infra/models/jira_jql.go @@ -0,0 +1,40 @@ +package models + +type ParsedQueryPageScheme struct { + Queries []*ParseQueryScheme `json:"queries"` +} + +type ParseQueryScheme struct { + Query string `json:"query"` + Structure struct { + Where struct { + } `json:"where"` + OrderBy *QueryStructureOrderScheme `json:"orderBy"` + } `json:"structure"` + Errors []string `json:"errors"` +} + +type QueryStructureScheme struct { + OrderBy *QueryStructureOrderScheme `json:"orderBy"` +} + +type QueryStructureOrderScheme struct { + Fields []*QueryStructureOrderFieldScheme `json:"fields"` +} + +type QueryStructureOrderFieldScheme struct { + Field *QueryStructureOrderFieldNodeScheme `json:"field"` + Direction string `json:"direction"` +} + +type QueryStructureOrderFieldNodeScheme struct { + Name string `json:"name"` + Property []*QueryPropertyScheme `json:"property"` +} + +type QueryPropertyScheme struct { + Entity string `json:"entity"` + Key string `json:"key"` + Path string `json:"path"` + Type string `json:"type"` +} diff --git a/service/jira/jql.go b/service/jira/jql.go new file mode 100644 index 00000000..d8eba896 --- /dev/null +++ b/service/jira/jql.go @@ -0,0 +1,18 @@ +package jira + +import ( + "context" + "github.com/ctreminiom/go-atlassian/pkg/infra/models" +) + +type JQLConnector interface { + + // Parse parses and validates JQL queries. + // + // Validation is performed in context of the current user. + // + // POST /rest/api/{2-3}/jql/parse + // + // https://docs.go-atlassian.io/jira-software-cloud/jql#parse-jql-query + Parse(ctx context.Context, validationType string, JqlQueries []string) (*models.ParsedQueryPageScheme, *models.ResponseScheme, error) +}