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 }