1 module dubproxy;
2 
3 import std.array : empty;
4 import std.typecons : nullable, Nullable;
5 import std.json;
6 import std.format : format, formattedWrite;
7 import std.file : exists, readText;
8 import std.exception : enforce;
9 import std.stdio;
10 
11 @safe:
12 
13 struct DubProxyFile {
14 	string[string] packages;
15 
16 	string getPath(string pkg) const {
17 		const string* pkgPath = pkg in this.packages;
18 		enforce(pkgPath, format!"No package with name '%s' found in DPF"(pkg));
19 		return *pkgPath;
20 	}
21 
22 	bool pkgExists(string pkg) const {
23 		return (pkg in this.packages) !is null;
24 	}
25 
26 	void insertPath(string pkg, string path) {
27 		const(string)* oldPath = pkg in this.packages;
28 		enforce(oldPath is null, format!"Package '%s' already with path '%s'"
29 				(pkg, *oldPath));
30 		this.packages[pkg] = path;
31 	}
32 
33 	void updatePath(string pkg, string path) {
34 		enforce(this.pkgExists(pkg), format!"Package '%s' must exist for update"
35 				(pkg));
36 		this.packages[pkg] = path;
37 	}
38 
39 	void removePackage(string pkg) {
40 		enforce(this.pkgExists(pkg), format!"Package '%s' does not exist in DPF"
41 				(pkg));
42 		this.packages.remove(pkg);
43 	}
44 }
45 
46 DubProxyFile fromFile(string path) @safe {
47 	enforce(exists(path), format!"No DPF exists with path '%s'"( path));
48 	return fromString(readText(path));
49 }
50 
51 DubProxyFile fromString(string jsonText) @safe {
52 	JSONValue j = parseJSON(jsonText);
53 
54 	enforce(j.type == JSONType.object, "Parsed DPF top level is "
55 			~ "not an object");
56 
57 	const(JSONValue)* pkg = "packages" in j;
58 	enforce(pkg !is null, "Key 'packages does not exist in parsed DPF");
59 	enforce(pkg.type == JSONType.object,
60 			"Value of 'packages' must be object");
61 
62 	DubProxyFile ret;
63 
64 	JSONValue pkgCopy = *pkg;
65 
66 	() @trusted {
67 		foreach(string key, ref JSONValue value; pkgCopy) {
68 			enforce(value.type == JSONType..string, format!
69 					("Value type to key '%s' was '%s', type string "
70 					 ~ "was expected")(key, value.type));
71 			ret.packages[key] = value.str();
72 		}
73 	}();
74 
75 	return ret;
76 }
77 
78 void toFile(const(DubProxyFile) dpf, string path) {
79 	auto f = File(path, "w");
80 	toImpl(f.lockingTextWriter(), dpf);
81 }
82 
83 string toString(const(DubProxyFile) dpf, string path) {
84 	import std.array : appender;
85 	auto app = appender!string();
86 	toImpl(app, dpf);
87 	return app.data;
88 }
89 
90 private void toImpl(LTW)(auto ref LTW ltw, const(DubProxyFile) dpf) {
91 	import std.algorithm.iteration : map, joiner;
92 	import std.algorithm.mutation : copy;
93 
94 	formattedWrite(ltw, "{\n\t\"packages\" : {\n");
95 	dpf.packages.byKeyValue()
96 		.map!(it => format!"\t\t\"%s\" : \"%s\""(it.key, it.value))
97 		.joiner(",\n")
98 		.copy(ltw);
99 	formattedWrite(ltw, "\n\t}");
100 	formattedWrite(ltw, "\n}");
101 }
102 
103 DubProxyFile getCodeDlangOrgCopy() {
104 	return parseCodeDlangOrgData(getCodeDlangOrgData());
105 }
106 
107 string getCodeDlangOrgData() @trusted {
108 	import std.exception : assumeUnique;
109 	import std.net.curl;
110 	import std.zlib;
111 
112 	auto data = get("https://code.dlang.org/api/packages/dump");
113 
114 	auto uc = new UnCompress();
115 
116 	const(void[]) un = uc.uncompress(data);
117 	return assumeUnique(cast(const(char)[])un);
118 }
119 
120 DubProxyFile parseCodeDlangOrgData(string data) {
121 	string fixUpKind(string kind) {
122 		switch(kind) {
123 			case "github": return "github.com";
124 			case "bitbucket": return "bitbucket.org";
125 			case "gitlab": return "gitlab.com";
126 			default:
127 				assert(false, kind);
128 		}
129 	}
130 
131 	JSONValue parsed = parseJSON(data);
132 	DubProxyFile ret;
133 
134 	enforce(parsed.type == JSONType.array,
135 			"Downloaded code.dlang.org dump was not an array");
136 
137 	foreach(it; parsed.arrayNoRef()) {
138 		enforce(it.type == JSONType.object,
139 				format!"Expected object got '%s' from '%s'"(it.type,
140 					it.toPrettyString()));
141 		auto name = "name" in it;
142 		enforce(name && name.type == JSONType..string,
143 				format!"no name found in '%s'"(it.toPrettyString()));
144 		string nameStr = name.str;
145 		//write(nameStr, " : ");
146 		auto repo = "repository" in it;
147 		if(repo && repo.type == JSONType.object) {
148 			auto kind = "kind" in (*repo);
149 			auto owner = "owner" in (*repo);
150 			auto project = "project" in (*repo);
151 
152 			enforce(kind && kind.type == JSONType..string,
153 					format!"kind was null in '%s'" (repo.toPrettyString()));
154 			enforce(owner && owner.type == JSONType..string,
155 					format!"owner was null in '%s'" (repo.toPrettyString()));
156 			enforce(project && project.type == JSONType..string,
157 					format!"project was null in '%s'" (repo.toPrettyString()));
158 
159 			string url = format!"https://%s/%s/%s.git"(fixUpKind(kind.str),
160 					owner.str, project.str);
161 			//writeln(url);
162 			ret.insertPath(nameStr, url);
163 		}
164 	}
165 
166 	return ret;
167 }