Skip to content

Commit

Permalink
Add ArticlesService in the client.
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.
  • Loading branch information
spl0i7 committed Sep 22, 2024
1 parent 874efaf commit 07bc21f
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 07bc21f

Please sign in to comment.