-
Notifications
You must be signed in to change notification settings - Fork 5
/
importer.clj
379 lines (336 loc) · 13.9 KB
/
importer.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
(ns io.randomseed.bankster.util.importer
^{:doc "Bankster library, import-export operations."
:author "Paweł Wilk"
:added "1.0.0"}
(:require [clojure.java.io :as io]
[clojure.data.csv :as csv]
[clojure.string :as str]
[trptr.java-wrapper.locale :as l]
[puget.printer :as puget]
[io.randomseed.bankster :as bankster]
[io.randomseed.bankster.scale :as scale]
[io.randomseed.bankster.registry :as registry]
[io.randomseed.bankster.currency :as currency]
[io.randomseed.bankster.util.fs :as fs]
[io.randomseed.bankster.util.map :as map]
[io.randomseed.bankster.util :refer :all])
(:import [io.randomseed.bankster Currency Registry]
[java.time LocalDateTime format.DateTimeFormatter]))
;;
;; Pathnames and URIs.
;;
(def ^{:const true :tag String :added "1.0.0"}
default-resource-name
"Name of a default resource container."
"io/randomseed/bankster")
(def ^{:const true :tag String :added "1.0.0"}
default-resource-must-exist-file
"Filename in a default resource container that must exist."
"config.edn")
(def ^{:const true :tag String :added "1.0.0"}
default-dump-filename
"Default EDN dump file."
"registry-dump.edn")
(def ^{:const true :tag String :added "1.0.0"}
default-export-filename
"Default EDN export file."
"registry-export.edn")
(def ^{:const true :tag String :added "1.0.0"}
default-reader-filenames
"Default data reader filenames (Clojure code)."
["data_readers.clj" "data_readers.cljc"])
(def ^{:const true :tag String :added "1.2.4"}
default-data-reader-filename
"Default data reader filename (pure data)."
"data_readers_edn.clj")
(def ^{:const true :tag String :added "1.0.0"}
default-handlers-pathname
"Default pathname of a reader handlers file."
"io/randomseed/bankster/money/reader_handlers.clj")
(def ^{:const true :tag String :added "1.0.0"}
default-handlers-namespace
"Default namespace of a reader handlers."
"io.randomseed.bankster.money")
(def ^{:const true :tag String :added "1.0.0"}
default-countries-csv
"Default CSV file with countries database."
"org/joda/money/CountryData.csv")
(def ^{:const true :tag String :added "1.0.0"}
default-currencies-csv
"Default CSV file with currencies database."
"org/joda/money/CurrencyData.csv")
;;
;; Transformation rules.
;;
(def ^{:private true :added "1.0.0"}
special-kinds
"ISO codes for special kinds of currencies."
{:USN :FIDUCIARY
:XSU :FIDUCIARY
:CLF :FIDUCIARY
:XUA :COMBANK
:XTS :EXPERIMENTAL
:XPT :COMMODITY
:XPD :COMMODITY
:XAU :COMMODITY
:XAG :COMMODITY
:XOF :FIAT
:XPF :FIAT
:XDR :FIDUCIARY
:XBA :FIDUCIARY
:XBB :FIDUCIARY
:XBC :FIDUCIARY
:XBD :FIDUCIARY
:XXX nil})
(defn make-currency
"Shapes an ISO-standardized currency entry. Gets a sequence of linear collections
describing currency and returns a currency object."
{:private true :added "1.0.0"}
[[id numeric scale domain]]
(when (some? id)
(let [id (keyword id)
numeric (or (try-parse-long numeric) currency/no-numeric-id)
numeric (if (< numeric 0) currency/no-numeric-id numeric)
scale (or (try-parse-int scale) currency/auto-scaled)
scale (if (< scale 0) currency/auto-scaled scale)
kind (get special-kinds id :FIAT)
domain :ISO-4217
weight 0]
(currency/new-currency id (long numeric) (int scale) kind domain (int weight)))))
;;
;; Joda Money CSV importer.
;;
(defn countries-load
"Reads CSV file in a format compliant with Joda Money and returns a map with currency
to countries associations where countries are as sets. The pathname should be
relative to resources directory."
{:added "1.0.0"}
([]
(countries-load nil))
([^String pathname]
(when-some [r (fs/paths->resource (or pathname default-countries-csv))]
(->> r fs/read-csv
(map (comp vec (partial map keyword)))
(into {})
(map/invert-in-sets)))))
(defn currencies-load
"Reads CSV file compliant with Joda Money and returns a map with currency
ID (keyword) as a key and currency data as its value (vector). The pathname should
be relative to resources directory."
{:added "1.0.0"}
([]
(currencies-load nil))
([^String pathname]
(when-some [f (fs/paths->resource (or pathname default-currencies-csv))]
(->> f fs/read-csv (map make-currency)))))
(defn joda-import
"Reads CSV files with countries and currencies definitions (Joda Money format) and
returns a registry."
{:tag Registry :added "1.0.0"}
([]
(joda-import nil nil))
([^String countries-pathname
^String currencies-pathname]
(let [^Registry registry (registry/new-registry)
^clojure.lang.PersistentHashMap countries (countries-load countries-pathname)
^clojure.lang.PersistentHashMap currencies (currencies-load currencies-pathname)]
(reduce (fn ^Registry [^Registry r, ^Currency c]
(currency/register r c (get countries (currency/id c))))
registry currencies))))
;;
;; EDN dumper and exporter.
;;
(defn currency->map
"Takes a currency and returns a map suitable for putting into a configuration
file. Extensions fields are ignored."
{:added "1.0.0"}
[{:keys [:numeric :scale :kind :weight]}]
(as-> (sorted-map) m
(if (and (number? numeric) (pos? numeric)) (assoc m :numeric numeric) m)
(if-not (and (some? scale) (neg? scale)) (assoc m :scale scale) m)
(if (some? kind) (assoc m :kind kind) m)
(if (and (number? weight) (not (zero? weight))) (assoc m :weight weight) m)))
(defn localized->map
"Takes a localized map entry (1st level) and returns a map suitable for putting into
a configuration file."
{:added "1.0.0"}
[m]
(map/map-keys (comp keyword str l/locale) m))
(defn registry->map
"Takes a registry and returns a map suitable for putting into a configuration
file. Extensions fields are ignored. When registry is not given it uses the global
one. Extension fields are ignored."
{:added "1.0.0"}
([]
(registry->map (registry/state)))
([^Registry registry]
(when (some? registry)
(sorted-map-by
#(compare %2 %1)
:version (. (LocalDateTime/now) format (DateTimeFormatter/ofPattern "YYYYMMddHHmmssSS"))
:localized (into (sorted-map) (map/map-vals localized->map (:cur-id->localized registry)))
:currencies (into (sorted-map) (map/map-vals currency->map (:cur-id->cur registry)))
:countries (into (sorted-map) (map/map-vals :id (:ctr-id->cur registry)))))))
(defn dump
"For the given filename (defaults to default-dump-filename) and a registry (defaults
to a global registry) creates a dump in EDN format.
Filename will be placed in the default directory of resources (the same that which
config.edn)."
{:added "1.0.0"}
([]
(dump default-dump-filename (registry/get)))
([^Registry registry]
(dump default-dump-filename registry))
([^String filename
^Registry registry]
(when-some [rdir (fs/resource-pathname default-resource-name
default-resource-must-exist-file)]
(let [pathname (io/file (.getParent ^java.io.File (io/file rdir)) filename)]
(println "Dumping registry to" (str pathname))
(spit pathname (puget/pprint-str registry))))))
(defn export
"For the given filename (defaults to default-dump-filename) and a registry (defaults
to a global registry) creates a configuration file in EDN format.
Filename will be placed in the default directory of resources (the same which holds
config.edn)."
{:added "1.0.0"}
([]
(export default-export-filename (registry/get)))
([^Registry registry]
(export default-export-filename registry))
([^String filename
^Registry registry]
(when-some [rdir (fs/resource-pathname default-resource-name
default-resource-must-exist-file)]
(let [pathname (io/file (.getParent ^java.io.File (io/file rdir)) filename)]
(println "Exporting configuration to" (str pathname))
(spit pathname (puget/pprint-str (registry->map registry)))))))
;;
;; Readers generator.
;;
(defn handler-preamble
"Preamble generator for a handler file."
{:no-doc true :added "1.0.0"}
([]
(handler-preamble default-handlers-namespace))
([handlers-namespace]
(let [nsp (symbol (str "'" handlers-namespace))]
(list 'in-ns nsp))))
(defn handler-gen-for-prefix
{:private true :added "1.0.0"}
[prefix names]
(map
(fn [n]
(list 'defn (symbol (str prefix "-" n))
'{:no-doc true}
'[arg] (list (symbol (str "ns-" prefix)) (str n) 'arg)))
names))
(defn handler-gen
"Generates handler functions for tagged literals for each namespaced currency. Each
function will have a prefixed name."
[names]
(concat (handler-gen-for-prefix "code-literal" names)
(handler-gen-for-prefix "data-literal" names)))
(defn readers-export
"Creates clojure source code files with reader functions for tagged literals handling
on a basis of registry information and data reader map files referring to the
created handlers.
The purpose of generation is primary to create handlers for literals in forms of
#money/NS[…], where NS is a namespace that corresponds to a namespace of a
currency. Possible namespaces are taken from a registry (a map from its field
.cur-id->cur).
The function takes a registry (defaults to a global registry if not given), a
sequence of reader filenames (defaults to default-reader-filenames), default
handlers pathname (defaults to default-handlers-pathname) and default handlers
namespace (defaults to default-handlers-namespace).
Default namespace is a namespace in which money handlers will be defined. These
handlers will be written to a file which pathname is constructed using the
following tactic:
1. Obtain the directory of the first filename from the given filenames list using
Java's resource lookup. The assumption is it should be src directory of
a project.
2. Append the file path passed as the handlers-pathname.
As for data reader map files, their directory name is also based on the lookup of
the first filename. Each filename will be populated with the same content which is
a map associating tagged literal with a function."
{:added "1.0.0"}
([]
(readers-export (registry/state)
default-reader-filenames
default-data-reader-filename
default-handlers-pathname
default-handlers-namespace))
([^Registry registry]
(readers-export registry
default-reader-filenames
default-data-reader-filename
default-handlers-pathname
default-handlers-namespace))
([^Registry registry filenames]
(readers-export registry
filenames
default-data-reader-filename
default-handlers-pathname
default-handlers-namespace))
([^Registry registry filenames data-filename]
(readers-export registry
filenames
data-filename
default-handlers-pathname
default-handlers-namespace))
([^Registry registry filenames data-filename handlers-pathname handlers-namespace]
(when-some [nsses (->> (.cur-id->cur ^Registry registry)
(map (comp namespace first))
(filter identity)
set seq)]
(let [m (->> nsses
(map #(vector (symbol "money" %) (symbol handlers-namespace (str "code-literal-" %))))
(into {'money 'io.randomseed.bankster.money/code-literal
'currency 'io.randomseed.bankster.currency/code-literal}))
dm (->> nsses
(map #(vector (symbol "money" %) (symbol handlers-namespace (str "data-literal-" %))))
(into {'money 'io.randomseed.bankster.money/data-literal
'currency 'io.randomseed.bankster.currency/data-literal}))]
(when-some [fdir (io/resource (first filenames))]
(when-some [pdir (.getParent (io/file fdir))]
(when-some [hfile (io/file pdir handlers-pathname)]
(println)
(println "------------- data readers map (for handling Clojure code):")
(println)
(puget/cprint m)
(println)
(println "------------- data readers map (for handling EDN data):")
(println)
(puget/cprint dm)
(println)
(doseq [f filenames]
(let [fname (io/file pdir f)]
(println "Exporting to:" (str fname))
(spit fname (puget/pprint-str m))))
(when (some? (seq data-filename))
(when-some [fname (io/file pdir data-filename)]
(println "Exporting to:" (str fname))
(spit fname (puget/pprint-str dm))))
(println)
(println "Generating handlers code to:" (str hfile))
(some->> nsses
(handler-gen)
(cons (handler-preamble handlers-namespace))
(map puget/pprint-str)
(str/join (str \newline \newline))
(spit hfile)))))))))
;;
;; High-level operations.
;;
(defn joda->bankster-dump
"Reads Joda Money CSV files and creates a registry dump named
resources/io/randomseed/bankster/registry-dump.edn."
{:added "1.0.0"}
[]
(println (time (dump (joda-import)))))
(defn joda->bankster-export
"Reads Joda Money CSV files and creates a configuration file named
resources/io/randomseed/bankster/registry-export.edn."
{:added "1.0.0"}
[]
(println (time (export (joda-import)))))