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