|
| 1 | +:Class GitHubAPIv3 |
| 2 | +⍝ This clas offers methods that allows one to communicate with GitHub by using GitHub's REST API from Dyalog APL. |
| 3 | +⍝ Note that this is version 3 of the GitHub API; the coming version 4 is **_not_** a REST interface!\\ |
| 4 | +⍝ Most methods of this class become available only by instanciating the class. At the moment this might seem |
| 5 | +⍝ unnecessary because there is only one constructor requiring an owner's name, but this will change once OAuth |
| 6 | +⍝ is going to be implemented.\\ |
| 7 | +⍝ The class came into being because APL Team needed such a class in order to deal with the members of the APLTree |
| 8 | +⍝ and the APL-cation projects.\\ |
| 9 | +⍝ Note that this class needs **at least Dyalog 16.0** (⎕JSON).\\ |
| 10 | +⍝ The project lives on <https://github.com/aplteam/GitHubAPIv3>\\ |
| 11 | +⍝ It is part of the [APLTree library](https://github.com/aplteam/apltree/wiki) |
| 12 | +⍝ Kai Jaeger ⋄ APL Team Ltd |
| 13 | + |
| 14 | + :Include ##.APLTreeUtils |
| 15 | + |
| 16 | + ⎕IO←1 ⋄ ⎕ML←1 |
| 17 | + |
| 18 | + ∇ r←Version |
| 19 | + :Access Public Shared |
| 20 | + r←'GitHub' '0.0.1.3' '2018-12-31' |
| 21 | + ∇ |
| 22 | + |
| 23 | + ∇ History |
| 24 | + :Access Public Shared |
| 25 | + ⍝ * 0.0.1 |
| 26 | + ⍝ First release. |
| 27 | + ∇ |
| 28 | + |
| 29 | + :Property owner |
| 30 | + ∇ r←Get |
| 31 | + r←_owner |
| 32 | + ∇ |
| 33 | + :EndProperty |
| 34 | + |
| 35 | + ∇ make1(owner) |
| 36 | + :Access Public Instance |
| 37 | + :Implements Constructor |
| 38 | + _owner←owner |
| 39 | + ∇ |
| 40 | + |
| 41 | + ∇ (rc msg ns)←GetLatestRelease repoName;gitPath |
| 42 | + :Access Public Instance |
| 43 | + ⍝ Returns data regarding the latest release. Drafts and beta releases are ignored.\\ |
| 44 | + ⍝ You must use https:// (protocol), or do not specify a protocol at all.\\ |
| 45 | + ⍝ `⍵` : Name of the repository.\\ |
| 46 | + ⍝ `←` : Same as `GetJson` - see there for details. |
| 47 | + gitPath←'https://api.github.com/repos/',_owner,'/',repoName,'/releases/latest' |
| 48 | + (rc msg ns)←GetJson gitPath |
| 49 | + :If 0=rc |
| 50 | + ns.⎕DF '[JSON object: Release (',repoName,'-',ns.tag_name,')]' |
| 51 | + :EndIf |
| 52 | + ∇ |
| 53 | + |
| 54 | + ∇ (rc msg ns)←GetReleaseByTagName(repoName tagName);gitPath |
| 55 | + :Access Public Instance |
| 56 | + ⍝ Fetches the release with `tagName` from `repoName`.\\ |
| 57 | + ⍝ You must use https:// (protocol), or do not specify a protocol at all. |
| 58 | + ⍝ ## Right argument |
| 59 | + ⍝ * Name of the repository |
| 60 | + ⍝ * The tag name |
| 61 | + ⍝ `←` : Same as `GetJson` - see there for details. |
| 62 | + gitPath←'https://api.github.com/repos/',_owner,'/',repoName,'/releases/tags/',tagName |
| 63 | + (rc msg ns)←GetJson gitPath |
| 64 | + :If 0=rc |
| 65 | + ns.⎕DF'[JSON object: release-',tagName,']' |
| 66 | + :EndIf |
| 67 | + ∇ |
| 68 | + |
| 69 | + ∇ (rc msg ns)←GetAllReleases repoName;gitPath |
| 70 | + :Access Public Instance |
| 71 | + ⍝ Returns all releases of a given repository.\\ |
| 72 | + ⍝ Notes: |
| 73 | + ⍝ * Pre-release are included |
| 74 | + ⍝ * Users without "Push" access will **_not_** see any draft releases\\ |
| 75 | + ⍝ You must use https:// (protocol), or do not specify a protocol at all. |
| 76 | + ⍝ `⍵` : Name of the repository.\\ |
| 77 | + ⍝ `←` : Same as `GetJson` - see there for details. |
| 78 | + gitPath←'https://api.github.com/repos/',_owner,'/',repoName,'/tags' |
| 79 | + (rc msg ns)←GetJson gitPath |
| 80 | + ∇ |
| 81 | + |
| 82 | + ∇ (rc msg ns)←GetAllRepos;gitPath |
| 83 | + :Access Public Instance |
| 84 | + ⍝ Returns data of all **public** repositories for the current owner.\\ |
| 85 | + gitPath←'https://api.github.com/users/',(_owner),'/repos' |
| 86 | + (rc msg ns)←GetJson gitPath |
| 87 | + ∇ |
| 88 | + |
| 89 | + ∇ number←CastTagname2Number text;vec;bool |
| 90 | + :Access Public Shared |
| 91 | + ⍝ Takes something like `v12.34.567` or `v123.4.5.6789` and returns 1234567 and 1234567.1234 respectively.\\ |
| 92 | + ⍝ The items are called major.minor.patch.built with "built" being optional.\\ |
| 93 | + ⍝ In case that is impossible (because `text` does not fulfil the criteria) `⍬` is returned.\\ |
| 94 | + ⍝ Assumptions: |
| 95 | + ⍝ * `text` may or may not start with a non-digit. If there is one it will be removed.\\ |
| 96 | + ⍝ Therefore both `1.2.3` and `v.1.2.3` are valid input. |
| 97 | + ⍝ * The remaining `text` must consist of nothing but digits and dots. |
| 98 | + ⍝ * The first two numbers ("major" and "minor") must not be bigger than 99. |
| 99 | + ⍝ * The third number ("path") must not be bigger than 999. |
| 100 | + ⍝ * The optional last (forth) number must not be bigger than 99999. |
| 101 | + ⍝ * `text` must come either with three numbers (as in `1.2.3`) or with four number (as in `1.2.3.9999`). |
| 102 | + ⍝ However, even if all assumptions are fulfilled but the result is zero there is still a `⍬` returned. |
| 103 | + ⍝ Note that leading zeros are mermitted. Therefor 1.2.3.001 is **_not_** a valid input. |
| 104 | + ⍝ Examples: |
| 105 | + ⍝ + 1.20.333 transforms into 120333 |
| 106 | + ⍝ + 12.12.123.12345 transforms into 1212123.12345 |
| 107 | + ⍝ If the tag name does not fulfil the assumptions the conversion might crash. In that case `⍬` is |
| 108 | + ⍝ returned as result. |
| 109 | + number←⍬ |
| 110 | + (bool vec)←'.'⎕VFI{⍵↓⍨~⎕D∊⍨⊃⍵}text |
| 111 | + :If 3 4∊⍨⍴bool |
| 112 | + vec←{⍵↑⍨3⌈4⌊⍴⍵}↑vec |
| 113 | + :If 3=⍴vec |
| 114 | + :If ∧/100 100 1000>vec |
| 115 | + :AndIf 0=number←100 100 1000⊥vec |
| 116 | + number←⍬ |
| 117 | + :EndIf |
| 118 | + :Else |
| 119 | + :If ~∧/100 100 1000 100000>vec |
| 120 | + number←⍬ |
| 121 | + :ElseIf 0=number←100 100 1000 100000⊥vec |
| 122 | + number←⍬ |
| 123 | + :EndIf |
| 124 | + number÷←100000 |
| 125 | + :EndIf |
| 126 | + :EndIf |
| 127 | + ∇ |
| 128 | + |
| 129 | + ∇ (rc msg endpoints)←GetAllEndpoints;gitPath;msg;rc |
| 130 | + :Access Public Shared |
| 131 | + ⍝ This method returns all REST endpoints offered by the API.\\ |
| 132 | + ⍝ It returns a namespace. Use `endpoint`'s built-in `List` function for an overview. |
| 133 | + ⍝ `endpoints` is empty in case of an error. |
| 134 | + gitPath←'https://api.github.com/' |
| 135 | + endpoints←⎕NS'' |
| 136 | + (rc msg endpoints)←GetJson gitPath |
| 137 | + :If 0=rc |
| 138 | + endpoints.⎕FX'r←List' 'r←{⍵,[1.5]⍎¨⍵}⎕NL-2' |
| 139 | + :EndIf |
| 140 | + ∇ |
| 141 | + |
| 142 | +⍝⍝⍝⍝⍝⍝⍝⍝⍝⍝⍝⍝⍝⍝ Private stuff |
| 143 | + |
| 144 | + GetNoOfPages←{ |
| 145 | + ⍝ Takes HTTP headers and returns the number if pages. If there are not any a zero is returned. |
| 146 | + headers←⍵ |
| 147 | + bool←'Link:'{⍺∘≡¨(⍴⍺)↑¨⍵}headers |
| 148 | + 0=+/bool:0 |
| 149 | + link←(bool⍳1)⊃headers |
| 150 | + buff←' 'Split link |
| 151 | + last←(¯1+⍴buff)⊃buff |
| 152 | + buff←{⍵{(⍴'?page-')↓⍵[2]↑⍵[1]↓⍺}⊃'(&|\?)page=[0-9]{1,}'⎕S 0 1⊣⍵}last |
| 153 | + ⊃⊃(//)⎕VFI buff |
| 154 | + } |
| 155 | + |
| 156 | + GetLinkToNextPage←{ |
| 157 | + ⍝ Takes HTTP headers and returns the link pointing to the next page, if any |
| 158 | + headers←⍵ |
| 159 | + bool←'Link:'{⍺∘≡¨(⍴⍺)↑¨⍵}headers |
| 160 | + 0=+/bool:0 |
| 161 | + link←(bool⍳1)⊃headers |
| 162 | + buff←' 'Split link |
| 163 | + 1↓¯2↓⊃1↓buff |
| 164 | + } |
| 165 | + |
| 166 | + SplitHeaders←{ |
| 167 | + headers←⍵↑⍨1⍳⍨(⎕UCS 10 13)⍷⍵ |
| 168 | + (⎕UCS 13 10)Split headers |
| 169 | + } |
| 170 | + |
| 171 | + ∇ txt←GetText obj;ts |
| 172 | + ts←⎕NEW StreamReader obj ⍝ text stream |
| 173 | + txt←ts.ReadToEnd |
| 174 | + ∇ |
| 175 | + |
| 176 | + ∇ (rc msg ns)←GetJson gitPath;cp;ServicePointManager;req;res;data;WebRequest;i;noOfPages;headers;owner |
| 177 | + ⍝ Takes a path which must specify a valid GitHub API URL and returns the data from GitHub.\\ |
| 178 | + ⍝ `⍵`: Project URL, for example 'api.github.com/repos/aplteam/testrepo/releases/latest' |
| 179 | + ⍝ `rc` |
| 180 | + ⍝ : Either 0 for okay or an error code.\\ |
| 181 | + ⍝ `msg` |
| 182 | + ⍝ : Is empty in case `rc←→0` but might offer addtional information otherwise. |
| 183 | + ⍝ `ns` |
| 184 | + ⍝ : Namespace with the data received from GitHub. |
| 185 | + :If 0=⎕NC'_owner' |
| 186 | + owner←'APL GitHub API' |
| 187 | + :Else |
| 188 | + owner←_owner |
| 189 | + :EndIf |
| 190 | + gitPath←'https://'{⍵,⍨⍺/⍨⍺≢(⍴⍺)↑⍵}Lowercase gitPath |
| 191 | + rc←0 |
| 192 | + msg←'' |
| 193 | + ns←⎕NS'' |
| 194 | + :Trap 90 |
| 195 | + ⎕USING←'System.Net,system.dll' 'System.IO' 'System.Text' |
| 196 | + cp←ServicePointManager.SecurityProtocol ⍝ current protocol |
| 197 | + ServicePointManager.SecurityProtocol←SecurityProtocolType.Tls12 |
| 198 | + req←WebRequest.CreateHttp⊂gitPath |
| 199 | + req.UserAgent←owner ⍝ MUST NOT be empty: required by the github api! |
| 200 | + req.Accept←'Accept: application/vnd.github.v3+json' |
| 201 | + res←req.GetResponse |
| 202 | + ServicePointManager.SecurityProtocol←cp |
| 203 | + :If res.StatusCode≠res.StatusCode.OK |
| 204 | + rc←res.StatusCode |
| 205 | + msg←'HTTP error' |
| 206 | + :Return |
| 207 | + :EndIf |
| 208 | + ns←⎕JSON GetText res.GetResponseStream |
| 209 | + headers←SplitHeaders⍕res.Headers |
| 210 | + :If 0<noOfPages←GetNoOfPages headers |
| 211 | + :For i :In 1↓⍳noOfPages |
| 212 | + req←WebRequest.CreateHttp⊂GetLinkToNextPage headers |
| 213 | + req.UserAgent←owner ⍝ MUST NOT be empty: required by the github api! |
| 214 | + res←req.GetResponse |
| 215 | + :If res.StatusCode≠res.StatusCode.OK |
| 216 | + rc←res.StatusCode |
| 217 | + msg←'HTTP error' |
| 218 | + :Return |
| 219 | + :EndIf |
| 220 | + ns,←⎕JSON GetText res.GetResponseStream |
| 221 | + :EndFor |
| 222 | + :EndIf |
| 223 | + :Else |
| 224 | + rc←90 |
| 225 | + msg←{⍵↓⍨1+⍵⍳':'}{⍵↑⍨1⍳⍨(⎕UCS 8 10)⍷⍵}⍕⎕EXCEPTION |
| 226 | + :EndTrap |
| 227 | + ∇ |
| 228 | + |
| 229 | +:EndClass |
0 commit comments