Repository: ctreminiom/go-atlassian
Pull request #158 (2022-10-28 17:45:11)

Feature/157


Patch:
diff --git a/jira/internal/jql_impl.go b/jira/internal/jql_impl.go
new file mode 100644
index 0000000..9f20462
--- /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/v2/api_client_impl.go b/jira/v2/api_client_impl.go
index 43489f1..1055338 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 7b1ccb3..1991166 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 0000000..fc13aa1
--- /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 0000000..d8eba89
--- /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)
+}


Test patch:
diff --git a/jira/internal/jql_impl_test.go b/jira/internal/jql_impl_test.go
new file mode 100644
index 0000000..e3fe501
--- /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)
+			}
+
+		})
+	}
+}