Skip to content

Commit

Permalink
Add ArticlesService in the client. (#309)
Browse files Browse the repository at this point in the history
Implement ArticlesService for managing Shopify articles, including
CRUD operations, tag related operations, and article count.

Co-authored-by: Oliver <[email protected]>
  • Loading branch information
spl0i7 and oliver006 authored Oct 10, 2024
1 parent 6243e33 commit e1f6563
Show file tree
Hide file tree
Showing 3 changed files with 345 additions and 0 deletions.
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

0 comments on commit e1f6563

Please sign in to comment.