Skip to content

Commit 7c38f4d

Browse files
committed
Added NewsAPI
1 parent d7ace9f commit 7c38f4d

File tree

9 files changed

+390
-3
lines changed

9 files changed

+390
-3
lines changed

pkg/client/transport.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"mime"
910
"net/http"
1011
"time"
1112
// Packages
@@ -90,8 +91,8 @@ func (transport *logtransport) RoundTrip(req *http.Request) (*http.Response, err
9091

9192
// If verbose is switched on, read the body
9293
if transport.v && resp.Body != nil {
93-
contentType := resp.Header.Get("Content-Type")
94-
if contentType == ContentTypeJson || contentType == ContentTypeTextPlain {
94+
contentType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
95+
if contentType == ContentTypeTextPlain || contentType == ContentTypeJson {
9596
defer resp.Body.Close()
9697
body, err := io.ReadAll(resp.Body)
9798
if err == nil {

pkg/multipart/multipart.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import (
1616
///////////////////////////////////////////////////////////////////////////////
1717
// TYPES
1818

19+
// Encoder is a multipart encoder object
1920
type Encoder struct {
2021
w *multipart.Writer
2122
}
2223

24+
// File is a file object, which is used to encode a file in a multipart request
2325
type File struct {
2426
Path string
2527
Body io.Reader
@@ -35,6 +37,7 @@ const (
3537
///////////////////////////////////////////////////////////////////////////////
3638
// LIFECYCLE
3739

40+
// NewEncoder creates a new encoder object, which writes to the io.Writer
3841
func NewEncoder(w io.Writer) *Encoder {
3942
return &Encoder{
4043
multipart.NewWriter(w),
@@ -44,6 +47,8 @@ func NewEncoder(w io.Writer) *Encoder {
4447
///////////////////////////////////////////////////////////////////////////////
4548
// PUBLIC METHODS
4649

50+
// Encode writes the struct to the multipart writer, including any File objects
51+
// which are added as form data and excluding any fields with a tag of json:"-"
4752
func (enc *Encoder) Encode(v any) error {
4853
rv := reflect.ValueOf(v)
4954
if rv.Kind() == reflect.Ptr {
@@ -82,7 +87,6 @@ func (enc *Encoder) Encode(v any) error {
8287
// If this is a file, then add it to the form data
8388
if field.Type == reflect.TypeOf(File{}) {
8489
path := value.(File).Path
85-
fmt.Println("path=", path)
8690
if part, err := enc.w.CreateFormFile(name, filepath.Base(path)); err != nil {
8791
result = errors.Join(result, err)
8892
} else if _, err := io.Copy(part, value.(File).Body); err != nil {
@@ -97,10 +101,12 @@ func (enc *Encoder) Encode(v any) error {
97101
return result
98102
}
99103

104+
// Return the MIME content type of the multipart writer
100105
func (enc *Encoder) ContentType() string {
101106
return enc.w.FormDataContentType()
102107
}
103108

109+
// Close the multipart writer after writing all the data
104110
func (enc *Encoder) Close() error {
105111
return enc.w.Close()
106112
}

pkg/newsapi/articles.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package newsapi
2+
3+
import (
4+
"time"
5+
6+
"github.com/mutablelogic/go-client/pkg/client"
7+
8+
// Namespace imports
9+
. "github.com/djthorpe/go-errors"
10+
)
11+
12+
///////////////////////////////////////////////////////////////////////////////
13+
// TYPES
14+
15+
type Article struct {
16+
Source Source `json:"source"`
17+
Title string `json:"title"`
18+
Author string `json:"author,omitempty"`
19+
Description string `json:"description,omitempty"`
20+
Url string `json:"url,omitempty"`
21+
ImageUrl string `json:"urlToImage,omitempty"`
22+
PublishedAt time.Time `json:"publishedAt,omitempty"`
23+
Content string `json:"content,omitempty"`
24+
}
25+
26+
type respArticles struct {
27+
Status string `json:"status"`
28+
Code string `json:"code,omitempty"`
29+
Message string `json:"message,omitempty"`
30+
TotalResults int `json:"totalResults"`
31+
Articles []Article `json:"articles"`
32+
}
33+
34+
///////////////////////////////////////////////////////////////////////////////
35+
// PUBLIC METHODS
36+
37+
// Returns headlines
38+
func (c *Client) Headlines(opt ...Opt) ([]Article, error) {
39+
var response respArticles
40+
var query opts
41+
42+
// Add options
43+
for _, opt := range opt {
44+
if err := opt(&query); err != nil {
45+
return nil, err
46+
}
47+
}
48+
49+
// Request -> Response
50+
if err := c.Do(nil, &response, client.OptPath("top-headlines"), client.OptQuery(query.Values())); err != nil {
51+
return nil, err
52+
} else if response.Status != "ok" {
53+
return nil, ErrBadParameter.Withf("%s: %s", response.Code, response.Message)
54+
}
55+
56+
// Return success
57+
return response.Articles, nil
58+
}
59+
60+
// Returns articles
61+
func (c *Client) Articles(opt ...Opt) ([]Article, error) {
62+
var response respArticles
63+
var query opts
64+
65+
// Add options
66+
for _, opt := range opt {
67+
if err := opt(&query); err != nil {
68+
return nil, err
69+
}
70+
}
71+
72+
// Request -> Response
73+
if err := c.Do(nil, &response, client.OptPath("everything"), client.OptQuery(query.Values())); err != nil {
74+
return nil, err
75+
} else if response.Status != "ok" {
76+
return nil, ErrBadParameter.Withf("%s: %s", response.Code, response.Message)
77+
}
78+
79+
// Return success
80+
return response.Articles, nil
81+
}

pkg/newsapi/articles_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package newsapi_test
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"testing"
7+
8+
// Packages
9+
opts "github.com/mutablelogic/go-client/pkg/client"
10+
newsapi "github.com/mutablelogic/go-client/pkg/newsapi"
11+
assert "github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_articles_001(t *testing.T) {
15+
assert := assert.New(t)
16+
client, err := newsapi.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
17+
assert.NoError(err)
18+
19+
articles, err := client.Headlines(newsapi.OptQuery("google"))
20+
assert.NoError(err)
21+
assert.NotNil(articles)
22+
23+
body, _ := json.MarshalIndent(articles, "", " ")
24+
t.Log(string(body))
25+
}
26+
27+
func Test_articles_002(t *testing.T) {
28+
assert := assert.New(t)
29+
client, err := newsapi.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
30+
assert.NoError(err)
31+
32+
articles, err := client.Articles(newsapi.OptQuery("google"), newsapi.OptLimit(1))
33+
assert.NoError(err)
34+
assert.NotNil(articles)
35+
36+
body, _ := json.MarshalIndent(articles, "", " ")
37+
t.Log(string(body))
38+
}

pkg/newsapi/client.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
newsapi implements an API client for NewsAPI (https://newsapi.org/docs)
3+
*/
4+
package newsapi
5+
6+
import (
7+
// Packages
8+
"github.com/mutablelogic/go-client/pkg/client"
9+
)
10+
11+
///////////////////////////////////////////////////////////////////////////////
12+
// TYPES
13+
14+
type Client struct {
15+
*client.Client
16+
}
17+
18+
///////////////////////////////////////////////////////////////////////////////
19+
// GLOBALS
20+
21+
const (
22+
endPoint = "https://newsapi.org/v2"
23+
)
24+
25+
///////////////////////////////////////////////////////////////////////////////
26+
// LIFECYCLE
27+
28+
func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) {
29+
// Create client
30+
client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptHeader("X-Api-Key", ApiKey))...)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
// Return the client
36+
return &Client{client}, nil
37+
}

pkg/newsapi/client_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package newsapi_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
// Packages
8+
opts "github.com/mutablelogic/go-client/pkg/client"
9+
newsapi "github.com/mutablelogic/go-client/pkg/newsapi"
10+
assert "github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_client_001(t *testing.T) {
14+
assert := assert.New(t)
15+
client, err := newsapi.New(GetApiKey(t), opts.OptTrace(os.Stderr, true))
16+
assert.NoError(err)
17+
assert.NotNil(client)
18+
t.Log(client)
19+
}
20+
21+
///////////////////////////////////////////////////////////////////////////////
22+
// ENVIRONMENT
23+
24+
func GetApiKey(t *testing.T) string {
25+
key := os.Getenv("NEWSAPI_KEY")
26+
if key == "" {
27+
t.Skip("NEWSAPI_KEY not set")
28+
t.SkipNow()
29+
}
30+
return key
31+
}

pkg/newsapi/opts.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package newsapi
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
)
7+
8+
///////////////////////////////////////////////////////////////////////////////
9+
// TYPES
10+
11+
type opts struct {
12+
Category string `json:"category,omitempty"`
13+
Language string `json:"language,omitempty"`
14+
Country string `json:"country,omitempty"`
15+
Query string `json:"q,omitempty"`
16+
Limit int `json:"pageSize,omitempty"`
17+
Sort string `json:"sortBy,omitempty"`
18+
}
19+
20+
// Opt is a function which can be used to set options on a request
21+
type Opt func(*opts) error
22+
23+
///////////////////////////////////////////////////////////////////////////////
24+
// METHODS
25+
26+
func (o *opts) Values() url.Values {
27+
result := url.Values{}
28+
if o.Category != "" {
29+
result.Set("category", o.Category)
30+
}
31+
if o.Language != "" {
32+
result.Set("language", o.Language)
33+
}
34+
if o.Country != "" {
35+
result.Set("country", o.Country)
36+
}
37+
if o.Query != "" {
38+
result.Set("q", o.Query)
39+
}
40+
if o.Limit > 0 {
41+
result.Set("pageSize", fmt.Sprint(o.Limit))
42+
}
43+
if o.Sort != "" {
44+
result.Set("sortBy", o.Sort)
45+
}
46+
return result
47+
}
48+
49+
///////////////////////////////////////////////////////////////////////////////
50+
// OPTIONS
51+
52+
// Set the category
53+
func OptCategory(v string) Opt {
54+
return func(o *opts) error {
55+
o.Category = v
56+
return nil
57+
}
58+
}
59+
60+
// Set the language
61+
func OptLanguage(v string) Opt {
62+
return func(o *opts) error {
63+
o.Language = v
64+
return nil
65+
}
66+
}
67+
68+
// Set the country
69+
func OptCountry(v string) Opt {
70+
return func(o *opts) error {
71+
o.Country = v
72+
return nil
73+
}
74+
}
75+
76+
// Set the query
77+
func OptQuery(v string) Opt {
78+
return func(o *opts) error {
79+
o.Query = v
80+
return nil
81+
}
82+
}
83+
84+
// Set the number of results
85+
func OptLimit(v int) Opt {
86+
return func(o *opts) error {
87+
o.Limit = v
88+
return nil
89+
}
90+
}
91+
92+
// Sort for articles by relevancy
93+
func OptSortByRelevancy() Opt {
94+
return func(o *opts) error {
95+
o.Sort = "relevancy"
96+
return nil
97+
}
98+
}
99+
100+
// Sort for articles by popularity
101+
func OptSortByPopularity() Opt {
102+
return func(o *opts) error {
103+
o.Sort = "popularity"
104+
return nil
105+
}
106+
}
107+
108+
// Sort for articles by date
109+
func OptSortByDate() Opt {
110+
return func(o *opts) error {
111+
o.Sort = "publishedAt"
112+
return nil
113+
}
114+
}

0 commit comments

Comments
 (0)