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 }