1 /** 2 Copyright: © 2013-2016 rejectedsoftware e.K. 3 License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file. 4 Authors: Colden Cullen 5 */ 6 module dubregistry.api; 7 8 import dubregistry.dbcontroller; 9 import dubregistry.registry; 10 11 import std.algorithm.iteration : map; 12 import std.array : array; 13 import std.exception : enforce; 14 import std.typecons : Flag, Yes, No; 15 import vibe.data.json : Json; 16 import vibe.http.router; 17 import vibe.inet.url; 18 import vibe.textfilter.urlencode; 19 import vibe.web.rest; 20 21 22 /** Registers the DUB registry REST API endpoints in the given router. 23 */ 24 void registerDubRegistryAPI(URLRouter router, DubRegistry registry) 25 { 26 auto pkgs = new LocalDubRegistryAPI(registry); 27 router.registerRestInterface(pkgs, "/api"); 28 router.get("/api/packages/dump", (req, res) @trusted => dumpPackages(req, res, registry)); 29 } 30 31 /// Compatibility alias. 32 deprecated("Use registerDubRegistryAPI instead.") 33 alias registerDubRegistryWebApi = registerDubRegistryAPI; 34 35 36 private void dumpPackages(HTTPServerRequest req, HTTPServerResponse res, DubRegistry registry) 37 { 38 import vibe.data.json : serializeToPrettyJson; 39 import vibe.stream.wrapper : streamOutputRange; 40 41 res.contentType = "application/json; charset=UTF-8"; 42 res.headers["Content-Encoding"] = "gzip"; // force GZIP compressed response 43 auto dst = streamOutputRange(res.bodyWriter); 44 dst.put('['); 45 bool first = true; 46 foreach (p; registry.getPackageDump()) { 47 if (!first) dst.put(','); 48 else first = false; 49 serializeToPrettyJson(&dst, p); 50 } 51 dst.put(']'); 52 } 53 54 /** Returns a REST client instance for communicating with a DUB registry's API. 55 56 Params: 57 url = URL of the DUB registry (e.g. "https://code.dlang.org/") 58 */ 59 DubRegistryAPI connectDubRegistryAPI(URL url) 60 { 61 return new RestInterfaceClient!DubRegistryAPI(url); 62 } 63 /// ditto 64 DubRegistryAPI connectDubRegistry(string url) 65 { 66 return connectDubRegistryAPI(URL(url)); 67 } 68 69 interface DubRegistryAPI { 70 @property IPackages packages(); 71 } 72 73 struct SearchResult { string name, description, version_; } 74 struct DownloadStats { DbDownloadStats downloads; } 75 alias Version = string; 76 77 interface IPackages { 78 @safe: 79 80 @method(HTTPMethod.GET) 81 SearchResult[] search(string q = ""); 82 83 @path(":name/latest") 84 string getLatestVersion(string _name); 85 86 @path(":name/stats") 87 DbPackageStats getStats(string _name); 88 89 @path(":name/:version/stats") 90 DownloadStats getStats(string _name, string _version); 91 92 @path(":name/info") 93 Json getInfo(string _name, bool minimize = false); 94 95 @path(":name/:version/info") 96 Json getInfo(string _name, string _version, bool minimize = false); 97 98 Json[string] getInfos(string[] packages, bool include_dependencies = false, bool minimize = false); 99 } 100 101 class LocalDubRegistryAPI : DubRegistryAPI { 102 private { 103 Packages m_packages; 104 } 105 106 this(DubRegistry registry) 107 { 108 m_packages = new Packages(registry); 109 } 110 111 @property Packages packages() { return m_packages; } 112 } 113 114 class Packages : IPackages { 115 private { 116 DubRegistry m_registry; 117 } 118 119 this(DubRegistry registry) 120 { 121 m_registry = registry; 122 } 123 124 override { 125 @method(HTTPMethod.GET) 126 SearchResult[] search(string q) { 127 return m_registry.searchPackages(q) 128 .map!(p => SearchResult(p.name, p.info["description"].opt!string, p.version_)) 129 .array; 130 } 131 132 string getLatestVersion(string name) { 133 return m_registry.getLatestVersion(rootOf(name)) 134 .check!(r => r.length)(HTTPStatus.notFound, "Package not found"); 135 } 136 137 DbPackageStats getStats(string name) { 138 try { 139 auto stats = m_registry.getPackageStats(rootOf(name)); 140 return stats; 141 } catch (RecordNotFound e) { 142 throw new HTTPStatusException(HTTPStatus.notFound, "Package not found"); 143 } 144 } 145 146 DownloadStats getStats(string name, string ver) { 147 try { 148 return typeof(return)(m_registry.getDownloadStats(rootOf(name), ver)); 149 } catch (RecordNotFound e) { 150 throw new HTTPStatusException(HTTPStatus.notFound, "Package or Version not found"); 151 } 152 } 153 154 Json getInfo(string name, bool minimize = false) { 155 immutable flags = minimize ? PackageInfoFlags.minimize : PackageInfoFlags.none; 156 return m_registry.getPackageInfo(rootOf(name), flags) 157 .check!(r => r.info.type != Json.Type.undefined)(HTTPStatus.notFound, "Package/Version not found") 158 .info; 159 } 160 161 Json getInfo(string name, string ver, bool minimize = false) { 162 immutable flags = minimize ? PackageInfoFlags.minimize : PackageInfoFlags.none; 163 return m_registry.getPackageVersionInfo(rootOf(name), ver, flags) 164 .check!(r => r.type != Json.Type.null_)(HTTPStatus.notFound, "Package/Version not found"); 165 } 166 167 Json[string] getInfos(string[] packages, bool include_dependencies = false, bool minimize = false) 168 { 169 import std.array : assocArray; 170 import std.typecons : tuple; 171 172 auto flags = minimize ? PackageInfoFlags.minimize : PackageInfoFlags.none; 173 if (include_dependencies) 174 flags |= PackageInfoFlags.includeDependencies; 175 return m_registry.getPackageInfosRecursive(packages, flags) 176 .check!(r => r !is null)(HTTPStatus.notFound, "None of the packages were found") 177 .byKeyValue.map!(p => tuple(p.key, p.value.info)).assocArray; 178 } 179 } 180 181 private: 182 string rootOf(string pkg) @safe { 183 import std.algorithm: findSplitBefore; 184 // FIXME: urlDecode should not be necessary, as the REST paramters are 185 // already decoded. 186 return pkg.urlDecode().findSplitBefore(":")[0]; 187 } 188 } 189 190 191 private auto ref T check(alias cond, T)(auto ref T t, HTTPStatus status, string msg) 192 { 193 enforce(cond(t), new HTTPStatusException(status, msg)); 194 return t; 195 }