-
Notifications
You must be signed in to change notification settings - Fork 2
/
things.clj
445 lines (380 loc) · 13.3 KB
/
things.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
(ns grimoire.things
"This namespace implements a \"thing\" structure, approximating a URI, for
uniquely naming and referencing entities in a Grimoire documentation
store.
Thing ::= Sum[Group, Artifact, Version, Platform,
Namespace, Def, Note, Example];
Group ::= Record[ Name: String];
Artifact ::= Record[Parent: Group, Name: String];
Version ::= Record[Parent: Artifact, Name: String];
Platform ::= Record[Parent: Version, Name: String];
Namespace ::= Record[Parent: Platform, Name: String];
Def ::= Record[Parent: Namespace, Name: String];
Note ::= Record[Parent: Thing, Handle: String];
Example ::= Record[Parent: Thing, Handle: String];"
(:refer-clojure :exclude [def namespace])
(:require [clojure.string :as string]
[clojure.core.match :refer [match]]
[grimoire.util :as u]
[guten-tag.core :as t]
[cemerick.url :as url]))
(t/deftag group
"Represents a Maven group."
[name]
{:pre [(string? name)]})
(t/deftag artifact
"Represents a Maven artifact, rooted on a group."
[parent, name]
{:pre [(group? parent)
(string? name)]})
(t/deftag version
"Represents a Maven version, rooted on an artifact."
[parent, name]
{:pre [(artifact? parent)
(string? name)]})
(t/deftag platform
"Represents a Clojure \"platform\" rooted on a version of an
artifact.
Platforms are a construct and represent a versioned set of
namespaces (and thus of defs) defining the versioned package at that
version. The idea is that a single artifact may have \"platform\"
code for any of Clojure, ClojureScript, ClojureCLR and soforth
simultaneously. Selecting a platform in a tree thus selects a set of
namespaces and defs which are particular to this platform. It also
allows Grimoire to host what would otherwise be name-colliding
functions which are really implicitly differentiated by platform."
[parent, name]
{:pre [(version? parent)
(string? name)]})
(t/deftag namespace
"Represents a Clojure \"namespace\" rooted on a platform in a
version of an artifact."
[parent, name]
{:pre [(platform? parent)
(string? name)]})
(t/deftag def
"Represents a Clojure \"Def\" rooted in a namespace on a platform in
a version of an artifact."
[parent, name]
{:pre [(namespace? parent)
(string? name)]})
(declare thing?)
(t/deftag note
"Represents a single block of notes on an arbitrary Thing as
identified by a Handle. The Handle is intended to be some structure
such as a file path, record ID, UUID or something else uniquely
naming a specific note."
[parent, name, handle]
{:pre [(thing? parent)
(string? name)
(string? handle)]})
(t/deftag example
"Represents a single example on an arbitrary Thing as identified by
a Handle. The Handle is intended to be some structure such as a file
path, record ID, UUID or other unique identifier for that singular
specific example."
[parent, name, handle]
{:pre [(thing? parent)
(string? name)
(string? handle)]})
;; Helpers for walking thing paths
(defn leaf?
"Predicate testing whether the input Thing is either an example or a
note."
[t]
(or (note? t)
(example? t)))
(defn namespaced?
"Predicate testing whether the input either is a namespace or has a namespace
as a parent."
[t]
(or (namespace? t)
(def? t)
(and (leaf? t)
(namespaced? (:parent t)))))
(defn platformed?
"Predicate testing whether the input either is a platform or has a platform as
a parent."
[t]
(or (namespaced? t)
(platform? t)
(and (leaf? t)
(platformed? (:parent t)))))
(defn versioned?
"Predicate testing whether the input exists within the subset of the \"thing\"
variant which can be said to be \"versioned\" in that it is rooted on a
Version instance and thus a version instance can be reached by upwards
traversal."
[t]
(or (platformed? t)
(version? t)
(and (leaf? t)
(versioned? (:parent t)))))
(defn artifacted?
"Predicate testing whether the input either is an artifact or has an artifact
as a parent."
[t]
(or (versioned? t)
(artifact? t)
(and (leaf? t)
(artifacted? (:parent t)))))
(defn grouped?
"Predicate testing whether the input either is a group or has a group as a
parent."
[t]
(or (artifacted? t)
(group? t)
(and (leaf? t)
(grouped? (:parent t)))))
(defn thing?
"Predicate testing whether the input exists within the \"thing\" variant of
Σ[Group, Artifact,Version, Platform, Namespace, Def]"
[t]
(grouped? t))
(defn thing->parent
"Function from any object to Maybe[Thing]. If the input is a thing, returns
the parent (maybe nil) of that Thing. Otherwise returns nil."
[t]
(when (thing? t)
(:parent t)))
(defn thing->name
"Function from an object to Maybe[String]. If the input is a thing, returns
the name of the Thing. Otherwise returns nil."
[t]
{:pre [(thing? t)]}
(:name t))
;; smarter url caching constructors
(declare thing->url-path)
(defn ->Group
([groupid]
{:pre [(string? groupid)]}
(let [v (->group groupid)]
(assoc v ::url (thing->url-path v))))
([_ groupid]
(->Group groupid)))
(defn ->Artifact
[group artifact]
(let [v (->artifact group artifact)]
(assoc v ::url (thing->url-path v))))
(defn ->Version
[artifact version]
(let [v (->version artifact version)]
(assoc v ::url (thing->url-path v))))
(defn ->Platform
[version platform]
(let [v (->platform version (u/normalize-platform platform))]
(assoc v ::url (thing->url-path v))))
(defn ->Ns
[platform namespace]
(let [v (->namespace platform namespace)]
(assoc v ::url (thing->url-path v))))
(defn ->Def
[namespace name]
(let [v (->def namespace name)]
(assoc v ::url (thing->url-path v))))
(defn ->Example
[thing name handle]
(let [v (->example thing name handle)]
(assoc v ::url handle)))
(defn ->Note
[thing name handle]
(let [v (->note thing name handle)]
(assoc v ::url handle)))
;; Manipulating things and strings
(defn thing->path
"Provides a mechanism for converting one of the Handle objects into a
cannonical \"path\" which can be serialized, deserialized and walked back into
a Handle."
[t]
{:pre [(thing? t)]}
(or (::url t)
(thing->url-path t)))
(defn path->thing
"String to Thing transformer which builds a Thing tree by splitting on /. The
resulting things are rooted on a Group as required by the definition of a
Thing."
[path]
(->> (string/split path #"/" 6)
(map vector [->Group ->Artifact ->Version ->Platform ->Ns ->Def])
(reduce (fn [acc [f v]]
(if v (f acc v) acc))
nil)))
(defn ensure-thing
"Transformer which, if given a string, will construct a Thing (with a warning)
and if given a Thing will return the Thing without modification. Intended as a
guard for potentially mixed input situations."
[maybe-thing]
(cond (string? maybe-thing)
,,(do (.write *err* "Warning: building a thing from a string via ensure-string!\n")
(path->thing maybe-thing))
(thing? maybe-thing)
,,maybe-thing
:else
,,(throw
(Exception.
(str "Unsupported ensure-thing value "
(pr-str maybe-thing))))))
;; Traversing things
(defn thing->group
"Function from a Thing to a Group. If the Thing is rooted on a Group,
or is a Group, traverses thing->parent until a Group is produced. Otherwise
returns nil."
[t]
{:pre [(thing? t)]}
(when (grouped? t)
(if-not (group? t)
(when t
(recur (thing->parent t)))
t)))
(defn thing->artifact
"Function from a Thing to an Artifact. If the Thing is rooted on an Artifact,
or is an Artifact, traverses thing->parent until the rooting Artifact is
reached and then returns that value. Otherwise returns nil."
[t]
{:pre [(thing? t)]}
(when (artifacted? t)
(if-not (artifact? t)
(when t
(recur (thing->parent t)))
t)))
(defn thing->version
"Function from a Thing to a Verison. If the Thing is rooted on a Version or is
a Version, traverses thing->parent until the rooting Version is reached and
then returns that value. Otherwise returns nil."
[t]
{:pre [(thing? t)]}
(when (versioned? t)
(if-not (version? t)
(when t
(recur (thing->parent t)))
t)))
(defn thing->platform
"Function from a Thing to a Platform. If the Thing is rooted on a Platform or
is a Platform traverses thing->parent until the rooting Platform is reached
and then returns that value. Otherwise returns nil."
[t]
{:pre [(thing? t)]}
(when (platformed? t)
(if-not (platform? t)
(when t
(recur (thing->parent t)))
t)))
(defn thing->namespace
"Function from a Thing to a Namespace. If the Thing is rooted on a Platform or
is a Platform traverses thing->parent until the rooting Platform is reached
and then returns that value. Otherwise returns nil."
[t]
{:pre [(thing? t)]}
(when (namespaced? t)
(if-not (namespace? t)
(when t
(recur (thing->parent t)))
t)))
(defn thing->def
"Function from a Thing to a Def. If the Thing either is a Def or is rooted on
a Def, traverses thing->parent until the rooting Def is reached and then
returns that value. Otherwise returns nil."
[t]
{:pre [(thing? t)]}
(when (def? t) t))
;; Bits and bats
(defn thing->url-path
"Function from a Thing to a munged and URL safe Thing path"
([t]
(thing->url-path t u/munge))
([t munge-fn]
{:pre [(thing? t)]}
(match [t]
[([::def {:name n :parent p}] :seq)]
,,(str (thing->url-path p) "/" (munge-fn n))
[([::group {:name n}] :seq)]
,,n
[([_ {:name n :parent p}] :seq)]
,,(str (thing->url-path p) "/" n))))
;; FIXME: this function could probably be a little more principled,
;; but so be it.
(defn url-path->thing
"Function from a URL to a Thing. Complement of thing->url-path."
[url]
(let [path-elems (string/split url #"/")
path-elems (if (<= 6 (count path-elems))
(concat
(take 5 path-elems)
[(u/unmunge (nth path-elems 5))]
(drop 6 path-elems))
path-elems)]
(path->thing (string/join "/" path-elems))))
(defn thing->type-name
[t]
{:pre [(thing? t)]}
(match [t]
[([::group {}] :seq)] "group"
[([::artifact {}] :seq)] "artifact"
[([::version {}] :seq)] "version"
[([::platform {}] :seq)] "platform"
[([::namespace {}] :seq)] "namespace"
[([::def {}] :seq)] "def"
[([::example {}] :seq)] "ex"))
(defn thing->full-uri
"Function from a Thing to a String representing a unique Thing naming URI.
URIs have the same structure as thing->url-path but are prefixed by <t>:
where <t> is the lower cased name of the type of the input Thing.
For example, a Thing represeting org.clojure/clojure would give the full URI
grim+artifact:org.clojure/clojure. A Thing representing org.clojure/clojure/1.6.0
would likewise be grim+version:org.clojure/clojure/1.6.0 and soforth."
[t]
{:pre [(thing? t)]}
(format "grim+%s:%s"
(thing->type-name t)
(thing->url-path t)))
(def full-uri-pattern
#"(grim\+(group|artifact|version|platform|namespace|def)(\+(note|example))?):([^?&#]+)([?&#].*)?")
(defn full-uri->thing
"Complement of thing->full-uri."
[uri-string]
{:pre [(string? uri-string)]}
(let [[_ scheme type _ extension path
:as groups] (re-find full-uri-pattern uri-string)]
(assert groups "Failed to parse URI. No regex match!")
(let [t (url-path->thing path)]
(assert (= (thing->type-name t) type)
"Failed to parse URI. Path didn't round trip to expected type!")
t)))
(def short-string-pattern
#"(\w{3,6})::([^\s/,;:\"'\[\]\(\)\s]+)(/([^,;:\"\[\]\(\)\s]+))?")
(defn thing->short-string
"Function from a Thing to a String representing a mostly unique naming string.
Unlike thing->full-uri, thing->short-string will discard exact artifact, group
and verison information instead giving only a URI with respect to the
platform, namespace and name of a Thing.
For example, the Thing representing
org.clojure/clojure/1.6.0/clj/clojure.core/+ would give the short string
clj::clojure.core/+."
[t]
{:pre [(platformed? t)]
:post [(re-find short-string-pattern %)]}
(match [t]
[([::namespace {:name nn
:parent {:name pn}}]
:seq)]
,,(format "%s::%s" pn nn)
[([::def {:name n
:parent {:name nn
:parent {:name pn}}}]
:seq)]
,,(format "%s::%s/%s"
pn nn n)))
;; short-string->thing to be defined in terms of a search for the latest version.
(defn parse-short-string
"Function from a String as generated by thing->short-string to one of
either [:def nil nil nil <ns-name> <def-name>] or [:ns nil nil nil
<ns-name>]. The intention is that this function can be used to parse
short-strings into structures for which Things can be looked up out of a
datastore. Returns nil on failure to parse."
[s]
{:pre [(string? s)]}
(let [[_s platform ns _ ?def :as match] (re-find short-string-pattern s)]
(when match
(if ?def
[:def nil nil nil platform ns ?def]
[:ns nil nil nil platform ns]))))