Repository: ctreminiom/go-atlassian
Pull request #327 (2024-10-06 00:36:58)

:sparkles: Mapped the new issue.Metadata methods.
- Resolves: #319
- Updated the README.md and added a new section to execute a RAW endpoints


Patch:
diff --git a/README.md b/README.md
index eeb38c3..e020f30 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,7 @@ instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN")
 
 ## ☕Cookbooks
 
-For detailed examples and usage of the go-atlassian library, please refer to our Cookbook. This section provides step-by-step guides and code samples for common tasks and scenarios.
+For detailed examples and usage of the go-atlassian library, please refer to our [**Cookbook**](https://docs.go-atlassian.io/cookbooks). This section provides step-by-step guides and code samples for common tasks and scenarios.
 
 -------------------------
 ## 🌍 Services
@@ -161,6 +161,65 @@ for _, transition := range issue.Transitions {
 
 The rest of the service functions work much the same way; they are concise and behave as you would expect. The [documentation](https://docs.go-atlassian.io/) contains several examples on how to use each service function.
 
+
+## 📪Call a RAW API Endpoint
+If you need to interact with an Atlassian API endpoint that hasn't been implemented in the `go-atlassian` library yet, you can make a custom API request using the built-in `Client.Call` method to execute raw HTTP requests.
+
+> Please raise an issue in order to implement the endpoint
+
+```go
+package main  
+  
+import (  
+    "context"  
+    "fmt" 
+    "github.com/ctreminiom/go-atlassian/jira/v3" 
+    "log" 
+    "net/http" 
+    "os"
+ )  
+  
+type IssueTypeMetadata struct {  
+    IssueTypes []struct {  
+       ID          string `json:"id"`  
+  Name        string `json:"name"`  
+  Description string `json:"description"`  
+  } `json:"issueTypes"`  
+}  
+  
+func main() {  
+  
+    var (  
+       host  = os.Getenv("SITE")  
+       mail  = os.Getenv("MAIL")  
+       token = os.Getenv("TOKEN")  
+    )  
+  
+    atlassian, err := v3.New(nil, host)  
+    if err != nil {  
+       log.Fatal(err)  
+    }  
+  
+    atlassian.Auth.SetBasicAuth(mail, token)  
+  
+    // Define the RAW endpoint  
+    apiEndpoint := "rest/api/3/issue/createmeta/KP/issuetypes"  
+  
+    request, err := atlassian.NewRequest(context.Background(), http.MethodGet, apiEndpoint, "", nil)  
+    if err != nil {  
+       log.Fatal(err)  
+    }  
+  
+    customResponseStruct := new(IssueTypeMetadata)  
+    response, err := atlassian.Call(request, &customResponseStruct)  
+    if err != nil {  
+       log.Fatal(err)  
+    }  
+  
+    fmt.Println(response.Status)  
+}
+```
+
 -------------------------
 ## ✍️ Contributions
 
diff --git a/jira/internal/metadata_impl.go b/jira/internal/metadata_impl.go
index b43622a..798d9c5 100644
--- a/jira/internal/metadata_impl.go
+++ b/jira/internal/metadata_impl.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"strconv"
 
 	"github.com/tidwall/gjson"
 
@@ -33,6 +34,8 @@ type MetadataService struct {
 
 // Get edit issue metadata returns the edit screen fields for an issue that are visible to and editable by the user.
 //
+// Deprecated. Please use Issue.Metadata.FetchIssueMappings() and Issue.Metadata.FetchFieldMappings() instead.
+//
 // Use the information to populate the requests in Edit issue.
 //
 // GET /rest/api/{2-3}/issue/{issueKeyOrID}/editmeta
@@ -44,6 +47,8 @@ func (m *MetadataService) Get(ctx context.Context, issueKeyOrID string, override
 
 // Create returns details of projects, issue types within projects, and, when requested,
 //
+// Deprecated. Please use Issue.Metadata.FetchIssueMappings() and Issue.Metadata.FetchFieldMappings() instead.
+//
 // the create screen fields for each issue type for the user.
 //
 // GET /rest/api/{2-3}/issue/createmeta
@@ -53,11 +58,113 @@ func (m *MetadataService) Create(ctx context.Context, opts *model.IssueMetadataC
 	return m.internalClient.Create(ctx, opts)
 }
 
+// FetchIssueMappings returns a page of issue type metadata for a specified project.
+//
+// Use the information to populate the requests in Create issue and Create issues.
+//
+// This operation can be accessed anonymously.
+//
+// GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/issuetypes
+//
+// Parameters:
+// - ctx: The context for the request.
+// - projectKeyOrID: The key or ID of the project.
+// - startAt: The starting index of the returned issues.
+// - maxResults: The maximum number of results to return.
+//
+// Returns:
+// - A gjson.Result containing the issue type metadata.
+// - A pointer to the response scheme.
+// - An error if the retrieval fails.
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-metadata-issue-types-for-a-project
+func (m *MetadataService) FetchIssueMappings(ctx context.Context, projectKeyOrID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) {
+	return m.internalClient.FetchIssueMappings(ctx, projectKeyOrID, startAt, maxResults)
+}
+
+// FetchFieldMappings returns a page of field metadata for a specified project and issue type.
+//
+// Use the information to populate the requests in Create issue and Create issues.
+//
+// This operation can be accessed anonymously.
+//
+// GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/fields/{issueTypeId}
+//
+// Parameters:
+// - ctx: The context for the request.
+// - projectKeyOrID: The key or ID of the project.
+// - issueTypeID: The ID of the issue type whose metadata is to be retrieved.
+// - startAt: The starting index of the returned fields.
+// - maxResults: The maximum number of results to return.
+//
+// Returns:
+// - A gjson.Result containing the field metadata.
+// - A pointer to the response scheme.
+// - An error if the retrieval fails.
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-field-metadata-for-a-project-and-issue-type-id
+func (m *MetadataService) FetchFieldMappings(ctx context.Context, projectKeyOrID, issueTypeID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) {
+	return m.internalClient.FetchFieldMappings(ctx, projectKeyOrID, issueTypeID, startAt, maxResults)
+}
+
 type internalMetadataImpl struct {
 	c       service.Connector
 	version string
 }
 
+func (i *internalMetadataImpl) FetchIssueMappings(ctx context.Context, projectKeyOrID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) {
+
+	if projectKeyOrID == "" {
+		return gjson.Result{}, nil, model.ErrNoProjectIDOrKey
+	}
+
+	params := url.Values{}
+	params.Add("startAt", strconv.Itoa(startAt))
+	params.Add("maxResults", strconv.Itoa(maxResults))
+
+	endpoint := fmt.Sprintf("rest/api/%v/issue/createmeta/%v/issuetypes?%v", i.version, projectKeyOrID, params.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return gjson.Result{}, nil, err
+	}
+
+	response, err := i.c.Call(request, nil)
+	if err != nil {
+		return gjson.Result{}, response, err
+	}
+
+	return gjson.ParseBytes(response.Bytes.Bytes()), response, nil
+}
+
+func (i *internalMetadataImpl) FetchFieldMappings(ctx context.Context, projectKeyOrID, issueTypeID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) {
+
+	if projectKeyOrID == "" {
+		return gjson.Result{}, nil, model.ErrNoProjectIDOrKey
+	}
+
+	if issueTypeID == "" {
+		return gjson.Result{}, nil, model.ErrNoIssueTypeID
+	}
+
+	params := url.Values{}
+	params.Add("startAt", strconv.Itoa(startAt))
+	params.Add("maxResults", strconv.Itoa(maxResults))
+	endpoint := fmt.Sprintf("rest/api/%v/issue/createmeta/%v/issuetypes/%v?%v", i.version, projectKeyOrID, issueTypeID, params.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return gjson.Result{}, nil, err
+	}
+
+	response, err := i.c.Call(request, nil)
+	if err != nil {
+		return gjson.Result{}, response, err
+	}
+
+	return gjson.ParseBytes(response.Bytes.Bytes()), response, nil
+}
+
 func (i *internalMetadataImpl) Get(ctx context.Context, issueKeyOrID string, overrideScreenSecurity, overrideEditableFlag bool) (gjson.Result, *model.ResponseScheme, error) {
 
 	if issueKeyOrID == "" {
diff --git a/service/jira/metadata.go b/service/jira/metadata.go
index bc3b5c2..bd998dc 100644
--- a/service/jira/metadata.go
+++ b/service/jira/metadata.go
@@ -27,4 +27,49 @@ type MetadataConnector interface {
 	//
 	// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-issue-metadata
 	Create(ctx context.Context, opts *model.IssueMetadataCreateOptions) (gjson.Result, *model.ResponseScheme, error)
+
+	// FetchIssueMappings returns a page of issue type metadata for a specified project.
+	//
+	// Use the information to populate the requests in Create issue and Create issues.
+	//
+	// This operation can be accessed anonymously.
+	//
+	// GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/issuetypes
+	//
+	// Parameters:
+	// - ctx: The context for the request.
+	// - projectKeyOrID: The key or ID of the project.
+	// - startAt: The starting index of the returned issues.
+	// - maxResults: The maximum number of results to return.
+	//
+	// Returns:
+	// - A gjson.Result containing the issue type metadata.
+	// - A pointer to the response scheme.
+	// - An error if the retrieval fails.
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-metadata-issue-types-for-a-project
+	FetchIssueMappings(ctx context.Context, projectKeyOrID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error)
+
+	// FetchFieldMappings returns a page of field metadata for a specified project and issue type.
+	//
+	// Use the information to populate the requests in Create issue and Create issues.
+	//
+	// This operation can be accessed anonymously.
+	//
+	// GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/fields/{issueTypeId}
+	//
+	// Parameters:
+	// - ctx: The context for the request.
+	// - projectKeyOrID: The key or ID of the project.
+	// - issueTypeID: The ID of the issue type whose metadata is to be retrieved.
+	// - startAt: The starting index of the returned fields.
+	// - maxResults: The maximum number of results to return.
+	//
+	// Returns:
+	// - A gjson.Result containing the field metadata.
+	// - A pointer to the response scheme.
+	// - An error if the retrieval fails.
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-field-metadata-for-a-project-and-issue-type-id
+	FetchFieldMappings(ctx context.Context, projectKeyOrID, issueTypeID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error)
 }


Test patch:
diff --git a/jira/internal/metadata_impl_test.go b/jira/internal/metadata_impl_test.go
index b4bd3fc..9fb976f 100644
--- a/jira/internal/metadata_impl_test.go
+++ b/jira/internal/metadata_impl_test.go
@@ -385,3 +385,363 @@ func Test_NewMetadataService(t *testing.T) {
 		})
 	}
 }
+
+func Test_internalMetadataImpl_FetchFieldMappings(t *testing.T) {
+	type fields struct {
+		c       service.Connector
+		version string
+	}
+	type args struct {
+		ctx            context.Context
+		projectKeyOrID string
+		issueTypeID    string
+		startAt        int
+		maxResults     int
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		want    gjson.Result
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the project key or ID is not provided",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "",
+				issueTypeID:    "10001",
+				startAt:        0,
+				maxResults:     50,
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     model.ErrNoProjectIDOrKey,
+		},
+		{
+			name:   "when the issue type ID is not provided",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				issueTypeID:    "",
+				startAt:        0,
+				maxResults:     50,
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     model.ErrNoIssueTypeID,
+		},
+		{
+			name:   "when the API version is v3",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				issueTypeID:    "10001",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/3/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: false,
+		},
+		{
+			name:   "when the API version is v2",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				issueTypeID:    "10001",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: false,
+		},
+		{
+			name:   "when the HTTP request cannot be created",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				issueTypeID:    "10001",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, errors.New("error"))
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     errors.New("error"),
+		},
+		{
+			name:   "when the HTTP call fails",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				issueTypeID:    "10001",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(nil, errors.New("error"))
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     errors.New("error"),
+		},
+	}
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			metadataService, err := NewMetadataService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResult, gotResponse, err := metadataService.FetchFieldMappings(testCase.args.ctx, testCase.args.projectKeyOrID, testCase.args.issueTypeID, testCase.args.startAt, testCase.args.maxResults)
+			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)
+				assert.Equal(t, gotResult, testCase.want)
+			}
+		})
+	}
+}
+
+func Test_internalMetadataImpl_FetchIssueMappings(t *testing.T) {
+	type fields struct {
+		c       service.Connector
+		version string
+	}
+	type args struct {
+		ctx            context.Context
+		projectKeyOrID string
+		startAt        int
+		maxResults     int
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		want    gjson.Result
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the project key or ID is not provided",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "",
+				startAt:        0,
+				maxResults:     50,
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     model.ErrNoProjectIDOrKey,
+		},
+		{
+			name:   "when the API version is v3",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/3/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: false,
+		},
+		{
+			name:   "when the API version is v2",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: false,
+		},
+		{
+			name:   "when the HTTP request cannot be created",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, errors.New("error"))
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     errors.New("error"),
+		},
+		{
+			name:   "when the HTTP call fails",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:            context.Background(),
+				projectKeyOrID: "DUMMY",
+				startAt:        0,
+				maxResults:     50,
+			},
+			on: func(fields *fields) {
+				client := mocks.NewConnector(t)
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(nil, errors.New("error"))
+				fields.c = client
+			},
+			want:    gjson.Result{},
+			wantErr: true,
+			Err:     errors.New("error"),
+		},
+	}
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			metadataService, err := NewMetadataService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResult, gotResponse, err := metadataService.FetchIssueMappings(testCase.args.ctx, testCase.args.projectKeyOrID, testCase.args.startAt, testCase.args.maxResults)
+			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)
+				assert.Equal(t, gotResult, testCase.want)
+			}
+		})
+	}
+}