Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ArticlesService in the client #309

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions articles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package goshopify

import (
"context"
"fmt"
"time"
)

const articlesBasePath = "articles"

// The ArticlesService allows you to create, publish, and edit articles on a shop's blog
// See: https://shopify.dev/docs/api/admin-rest/stable/resources/article
type ArticlesService interface {
List(context.Context, uint64, interface{}) ([]Article, error)
Create(context.Context, uint64, Article) (*Article, error)
Get(context.Context, uint64, uint64) (*Article, error)
Update(context.Context, uint64, uint64, Article) (*Article, error)
Delete(context.Context, uint64, uint64) error
Count(context.Context, uint64, interface{}) (int, error)
ListTags(context.Context, interface{}) ([]string, error)
ListBlogTags(context.Context, uint64, interface{}) ([]string, error)
}

type ArticleResource struct {
Article *Article `json:"article"`
}

type ArticlesResource struct {
Articles []Article `json:"articles"`
}

// ArticlesServiceOp handles communication with the articles related methods of
// the Shopify API.
type ArticlesServiceOp struct {
client *Client
}

type ArticleTagsResource struct {
Tags []string `json:"tags,omitempty"`
}

type ArticleImage struct {
CreatedAt *time.Time `json:"created_at,omitempty"`
Alt string `json:"alt,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Src string `json:"src,omitempty"`
}

type MetaFields struct {
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
Type string `json:"type,omitempty"`
Namespace string `json:"namespace,omitempty"`
}

type Article struct {
Author string `json:"author,omitempty"`
BlogId uint64 `json:"blog_id,omitempty"`
BodyHtml string `json:"body_html,omitempty"`
Id uint64 `json:"id,omitempty"`
Handle string `json:"handle,omitempty"`
Image *ArticleImage `json:"image,omitempty"`
Metafields *MetaFields `json:"metafields"`
Published bool `json:"published,omitempty"`
SummaryHtml string `json:"summary_html,omitempty"`
Tags string `json:"tags,omitempty"`
Title string `json:"title,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UserId int `json:"user_id,omitempty"`
PublishedAt *time.Time `json:"published_at,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}

// List all the articles in a blog.
func (s *ArticlesServiceOp) List(ctx context.Context, blogId uint64, options interface{}) ([]Article, error) {
path := fmt.Sprintf("%s/%d/%s.json", blogsBasePath, blogId, articlesBasePath)
resource := new(ArticlesResource)
err := s.client.Get(ctx, path, resource, options)
return resource.Articles, err
}

// Create a article in a blog.
func (s *ArticlesServiceOp) Create(ctx context.Context, blogId uint64, article Article) (*Article, error) {
path := fmt.Sprintf("%s/%d/%s.json", blogsBasePath, blogId, articlesBasePath)
body := ArticleResource{
Article: &article,
}
resource := new(ArticleResource)
err := s.client.Post(ctx, path, body, resource)
return resource.Article, err
}

// Get an article by blog id and article id.
func (s *ArticlesServiceOp) Get(ctx context.Context, blogId uint64, articleId uint64) (*Article, error) {
path := fmt.Sprintf("%s/%d/%s/%d.json", blogsBasePath, blogId, articlesBasePath, articleId)
resource := new(ArticleResource)
err := s.client.Get(ctx, path, resource, nil)
return resource.Article, err
}

// Update an article in a blog.
func (s *ArticlesServiceOp) Update(ctx context.Context, blogId uint64, articleId uint64, article Article) (*Article, error) {
path := fmt.Sprintf("%s/%d/%s/%d.json", blogsBasePath, blogId, articlesBasePath, articleId)
wrappedData := ArticleResource{Article: &article}
resource := new(ArticleResource)
err := s.client.Put(ctx, path, wrappedData, resource)
return resource.Article, err
}

// Delete an article in a blog.
func (s *ArticlesServiceOp) Delete(ctx context.Context, blogId uint64, articleId uint64) error {
path := fmt.Sprintf("%s/%d/%s/%d.json", blogsBasePath, blogId, articlesBasePath, articleId)
return s.client.Delete(ctx, path)
}

// ListTags Get all tags from all articles.
func (s *ArticlesServiceOp) ListTags(ctx context.Context, options interface{}) ([]string, error) {
path := fmt.Sprintf("%s/tags.json", articlesBasePath)
articleTags := new(ArticleTagsResource)
err := s.client.Get(ctx, path, &articleTags, options)
return articleTags.Tags, err
}

// Count Articles from a Blog.
func (s *ArticlesServiceOp) Count(ctx context.Context, blogId uint64, options interface{}) (int, error) {
path := fmt.Sprintf("%s/%d/%s/count.json", blogsBasePath, blogId, articlesBasePath)
return s.client.Count(ctx, path, options)
}

// ListBlogTags Get all tags from all articles in a blog.
func (s *ArticlesServiceOp) ListBlogTags(ctx context.Context, blogId uint64, options interface{}) ([]string, error) {
path := fmt.Sprintf("%s/%d/%s/tags.json", blogsBasePath, blogId, articlesBasePath)
articleTags := new(ArticleTagsResource)
err := s.client.Get(ctx, path, &articleTags, options)
return articleTags.Tags, err
}
206 changes: 206 additions & 0 deletions articles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package goshopify

import (
"context"
"fmt"
"reflect"
"testing"

"github.com/jarcoal/httpmock"
)

func TestArticleList(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"GET",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles.json", client.pathPrefix),
httpmock.NewStringResponder(
200,
`{"articles": [{"id":1},{"id":2}]}`,
),
)

articles, err := client.Article.List(context.Background(), 241253187, nil)
if err != nil {
t.Errorf("Article.List returned error: %v", err)
}

expected := []Article{
{
Id: 1,
},
{
Id: 2,
},
}
if !reflect.DeepEqual(articles, expected) {
t.Errorf("Articles.List returned %+v, expected %+v", articles, expected)
}
}

func TestArticleCreate(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"POST",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles.json", client.pathPrefix),
httpmock.NewStringResponder(
201,
`{"article": {"id": 1}}`,
),
)

article := Article{Title: "Test Article"}
createdArticle, err := client.Article.Create(context.Background(), 241253187, article)
if err != nil {
t.Errorf("Article.Create returned error: %v", err)
}

expected := &Article{Id: 1}
if !reflect.DeepEqual(createdArticle, expected) {
t.Errorf("Article.Create returned %+v, expected %+v", createdArticle, expected)
}
}

func TestArticleGet(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"GET",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles/1.json", client.pathPrefix),
httpmock.NewStringResponder(
200,
`{"article": {"id": 1, "title": "Test Article"}}`,
),
)

article, err := client.Article.Get(context.Background(), 241253187, 1)
if err != nil {
t.Errorf("Article.Get returned error: %v", err)
}

expected := &Article{Id: 1, Title: "Test Article"}
if !reflect.DeepEqual(article, expected) {
t.Errorf("Article.Get returned %+v, expected %+v", article, expected)
}
}

func TestArticleUpdate(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"PUT",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles/1.json", client.pathPrefix),
httpmock.NewStringResponder(
200,
`{"article": {"id": 1, "title": "Updated Article"}}`,
),
)

article := Article{Title: "Updated Article"}
updatedArticle, err := client.Article.Update(context.Background(), 241253187, 1, article)
if err != nil {
t.Errorf("Article.Update returned error: %v", err)
}

expected := &Article{Id: 1, Title: "Updated Article"}
if !reflect.DeepEqual(updatedArticle, expected) {
t.Errorf("Article.Update returned %+v, expected %+v", updatedArticle, expected)
}
}

func TestArticleDelete(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"DELETE",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles/1.json", client.pathPrefix),
httpmock.NewStringResponder(
204, // No content response
``,
),
)

err := client.Article.Delete(context.Background(), 241253187, 1)
if err != nil {
t.Errorf("Article.Delete returned error: %v", err)
}
}

func TestArticleListTags(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"GET",
fmt.Sprintf("https://fooshop.myshopify.com/%s/articles/tags.json", client.pathPrefix),
httpmock.NewStringResponder(
200,
`{"tags": ["tag1", "tag2"]}`,
),
)

tags, err := client.Article.ListTags(context.Background(), nil)
if err != nil {
t.Errorf("Article.ListTags returned error: %v", err)
}

expected := []string{"tag1", "tag2"}
if !reflect.DeepEqual(tags, expected) {
t.Errorf("Article.ListTags returned %+v, expected %+v", tags, expected)
}
}

func TestArticleCount(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"GET",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles/count.json", client.pathPrefix),
httpmock.NewStringResponder(
200,
`{"count": 2}`,
),
)

count, err := client.Article.Count(context.Background(), 241253187, nil)
if err != nil {
t.Errorf("Article.Count returned error: %v", err)
}

expected := 2
if count != expected {
t.Errorf("Article.Count returned %d, expected %d", count, expected)
}
}

func TestArticleListBlogTags(t *testing.T) {
setup()
defer teardown()

httpmock.RegisterResponder(
"GET",
fmt.Sprintf("https://fooshop.myshopify.com/%s/blogs/241253187/articles/tags.json", client.pathPrefix),
httpmock.NewStringResponder(
200,
`{"tags": ["blogTag1", "blogTag2"]}`,
),
)

tags, err := client.Article.ListBlogTags(context.Background(), 241253187, nil)
if err != nil {
t.Errorf("Article.ListBlogTags returned error: %v", err)
}

expected := []string{"blogTag1", "blogTag2"}
if !reflect.DeepEqual(tags, expected) {
t.Errorf("Article.ListBlogTags returned %+v, expected %+v", tags, expected)
}
}
2 changes: 2 additions & 0 deletions goshopify.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ type Client struct {
PaymentsTransactions PaymentsTransactionsService
OrderRisk OrderRiskService
ApiPermissions ApiPermissionsService
Article ArticlesService
}

// A general response error that follows a similar layout to Shopify's response
Expand Down Expand Up @@ -336,6 +337,7 @@ func NewClient(app App, shopName, token string, opts ...Option) (*Client, error)
c.PaymentsTransactions = &PaymentsTransactionsServiceOp{client: c}
c.OrderRisk = &OrderRiskServiceOp{client: c}
c.ApiPermissions = &ApiPermissionsServiceOp{client: c}
c.Article = &ArticlesServiceOp{client: c}

// apply any options
for _, opt := range opts {
Expand Down
Loading