1 /**
2  * Handle meta information related to pulling articles from Dev.to
3  */
4 module devtopull;
5 
6 import std.datetime;
7 import std.range;
8 
9 import vibe.data.json;
10 
11 import devto.api;
12 
13 version(unittest) {
14     import unittesting.articlegenerator;
15     import std.algorithm : map, until, equal, fold;
16 }
17 
18 /**
19  * Provides information about the last execution to pull.
20  *
21  * This structure will allow retrieving newer articles since the last session.
22  */
23 struct PullState {
24     /// When the pull was executed
25     SysTime datePulled;
26     /// The newest article ID obtained during the pulling
27     uint lastArticle;
28 }
29 
30 /**
31  * Determine if an article is newer than the last PullState.
32  */
33 @safe pure nothrow @nogc
34 auto isNewerArticle(const ArticleMe am, const PullState ps) {
35     if(!am.published) return true;
36     return am.published_timestamp > ps.datePulled;
37 } unittest {
38     PullState ps;
39     ps.datePulled = SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC());
40     alias inPreviousPull = (x) => !x.isNewerArticle(ps);
41     auto devreq = generate!fakeArticleMe
42         .seqence(ps.datePulled)
43         .map!(x => x.deserializeJson!(ArticleMe))
44         .take(3);
45     assert(devreq.until!inPreviousPull.empty);
46 } unittest {
47     PullState ps;
48     ps.datePulled = SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC());
49     alias inPreviousPull = (x) => !x.isNewerArticle(ps);
50     auto devreq = generate!fakeArticleMe
51         .seqence(ps.datePulled + dur!"days"(1))
52         .map!(x => x.deserializeJson!(ArticleMe))
53         .take(3)
54         .map!(x => cast(immutable(ArticleMe))x);
55     assert(devreq.until!inPreviousPull.map!(x => x.published_timestamp).equal([ps.datePulled + dur!"days"(1)]));
56 }
57 
58 /**
59  * Deserializes file into the PullState structure
60  */
61 @safe
62 PullState readLastPull(string filename) {
63     import std.file : readText;
64     return deserializeJson!(PullState)(readText(filename));
65 }
66 
67 /**
68  * Serializes PullState structure into file
69  */
70 @trusted
71 void saveLastPull(const PullState ps, string filename) {
72     import util.file : fileWriter;
73     import std.algorithm : copy;
74     serializeToPrettyJson(ps).copy(fileWriter(filename));
75 }
76 
77 struct DevToRange {
78     private const(ArticleMe)[] function(uint page) @safe articlePage;
79 
80     const(ArticleMe)[] data;
81     uint page = 1;
82 
83     @safe nothrow pure const
84     const(ArticleMe) front() {
85         return data.front;
86     }
87 
88     @safe nothrow pure const
89     bool empty() {
90         return data.empty;
91     }
92 
93     @safe
94     void popFront() {
95         data.popFront;
96         if(data.empty)
97             data = articlePage(++page);
98     }
99 } unittest {
100     alias fakeArticles = (uint page) @safe {
101         return () @trusted {
102         static iteration = 0;
103         if(iteration++ < 1)
104         return generate!fakeArticleMe
105             .map!(x => cast(immutable(ArticleMe))x.deserializeJson!(ArticleMe))
106             .take(3)
107             .array;
108         return generate!fakeArticleMe
109             .map!(x => cast(immutable(ArticleMe))x.deserializeJson!(ArticleMe))
110             .take(0)
111             .array;
112         }();
113     };
114 
115     DevToRange dtv;
116     dtv.articlePage = fakeArticles;
117     dtv.data = dtv.articlePage(1);
118 
119     import std.algorithm : count;
120     assert(dtv.count == 3);
121 } unittest {
122     alias fakeArticles = (uint page) @safe {
123         return () @trusted {
124         static iteration = 0;
125         if(iteration++ < 2)
126         return generate!fakeArticleMe
127             .map!(x => cast(immutable(ArticleMe))x.deserializeJson!(ArticleMe))
128             .take(3)
129             .array;
130         return generate!fakeArticleMe
131             .map!(x => cast(immutable(ArticleMe))x.deserializeJson!(ArticleMe))
132             .take(0)
133             .array;
134         }();
135     };
136 
137     DevToRange dtv;
138     dtv.articlePage = fakeArticles;
139     dtv.data = dtv.articlePage(1);
140 
141     import std.algorithm : count;
142     assert(dtv.count == 6);
143 }
144 
145 @safe
146 DevToRange devtoMyArticles() {
147     DevToRange dtv;
148     dtv.articlePage = &devArticles;
149     dtv.data = dtv.articlePage(1);
150     return dtv;
151 }
152 
153 @safe
154 const(ArticleMe)[] devArticles(uint page) {
155     import vibe.core.log;
156     import vibe.http.client;
157     import std.process;
158     import std.format;
159 
160     const(ArticleMe)[] ret;
161 
162     requestHTTP(format!"https://dev.to/api/articles/me/all?page=%s"(page),
163         (scope req) @safe {
164             req.headers.addField("api-key", environment["appkey"]);
165         },
166         (scope res) @safe {
167             if(res.statusCode != 200)
168                 return;
169                 import std.algorithm : each;
170             auto data = res.readJson().get!(Json[]);
171             data.each!(x => x["published_timestamp"] = x["published_timestamp"].get!string.empty ?  SysTime.init.toISOExtString : x["published_timestamp"].get!string);
172             ret = meArticles(Json(data));
173         }
174     );
175 
176     return ret;
177 }