ci: do not error when nothing with nitunit_some
[nit.git] / contrib / refund / src / refund_json.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2015 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # JSON handling for `refund`.
18 module refund_json
19
20 import refund_base
21 import json::static
22 import json
23
24 redef class RefundProcessor
25
26 redef fun process(input_file, output_file) do
27 self.output_file = output_file
28 var json = load_input(input_file)
29 var sheet = new ReclamationSheet.from_json(self, json)
30 var res = process_refunds(sheet)
31 write_output(res.to_pretty_json, output_file)
32 end
33
34 # Computes allowed refunds for a given `ReclamationSheet`.
35 fun process_refunds(sheet: ReclamationSheet): JsonObject do
36 # update stats
37 var stats = load_stats
38 stats.inc("total_treatments")
39 # compute refunds
40 current_refunds.clear
41 var json = new JsonObject
42 json["dossier"] = sheet.file.to_s
43 json["mois"] = sheet.month.to_s
44 var arr = new JsonArray
45 var sum = 0.0.to_dollar
46 for recl in sheet.recls do
47 var refund = process_refund(sheet, recl)
48 var obj = new JsonObject
49 obj["soin"] = recl.care_id
50 obj["date"] = recl.date.to_s
51 obj["montant"] = refund.to_s
52 arr.add obj
53 sum += refund
54 # update stats for care
55 stats.inc("total_{recl.care_id}")
56 end
57 save_stats(stats)
58 json["remboursements"] = arr
59 json["total"] = sum.to_s
60 return json
61 end
62
63 # Loads the input string and returns its content as a JsonObject.
64 #
65 # Dies if the file cannot be read or does not contain a valid JSONObject.
66 fun load_input(file: String): JsonObject do
67 if not file.file_exists then
68 die("File `{file}` not found.")
69 abort
70 end
71 var ptr = new FileReader.open(file)
72 var json = ptr.read_all.parse_json
73 if json isa JsonParseError then
74 die("Wrong input file ({json.message})")
75 abort
76 else if json == null then
77 die("Unable to parse input file as json (got null)")
78 abort
79 else if not json isa JsonObject then
80 die("Wrong input type (expected JsonObject got {json.class_name})")
81 abort
82 end
83 ptr.close
84 return json
85 end
86
87 # Writes `str` in path specified by `file`.
88 #
89 # Used to produce output and stats.
90 fun write_output(str: String, file: String) do
91 var ofs = new FileWriter.open(file)
92 ofs.write(str)
93 ofs.write("\n")
94 ofs.close
95 end
96
97 # UTILS
98
99 # Does `json` contains `key`? Dies otherwise.
100 private fun check_key(json: JsonObject, key: String) do
101 if json.has_key(key) then return
102 die("Malformed input (missing key {key})")
103 end
104
105 # Does `str` match the regex `re`.
106 private fun check_format(str, re: String): Bool do
107 return str.has(re.to_re)
108 end
109
110 redef fun die(msg) do
111 # save error
112 var obj = new JsonObject
113 obj["message"] = msg
114 write_output(obj.to_pretty_json, output_file)
115 # update stats
116 var stats = load_stats
117 stats.inc("total_reject")
118 save_stats(stats)
119 # leave
120 exit 1
121 end
122
123 redef fun show_stats do print load_stats.to_json_object.to_pretty_json
124
125 redef fun load_stats do
126 # If no stats found, return a new object
127 if not stats_file.file_exists then return new RefundStats
128 # Try to read from file
129 var ifs = new FileReader.open(stats_file)
130 var content = ifs.read_all.parse_json
131 ifs.close
132 # If file is corrupted, return a new object
133 if not content isa JsonObject then return new RefundStats
134 # Return file contained stats
135 return new RefundStats.from_json(content)
136 end
137
138 redef fun save_stats(stats) do
139 write_output(stats.to_json_object.to_pretty_json, stats_file)
140 end
141 end
142
143 redef class RefundStats
144
145 # Inits `self` from the content of a JsonObject
146 init from_json(json: JsonObject) do
147 for k, v in json do self[k] = v.as(Int)
148 end
149
150 # Outputs `self` as a JSON string.
151 fun to_json_object: JsonObject do
152 var obj = new JsonObject
153 for k, v in self do obj[k] = v
154 return obj
155 end
156 end
157
158 redef class ReclamationSheet
159
160 # Inits `self` from the content of a `JsonObject`.
161 init from_json(proc: RefundProcessor, json: JsonObject) do
162 file = new ReclFile.from_json(proc, json)
163 month = new ReclMonth.from_json(proc, json)
164 recls = parse_recls(proc, json)
165 init(file, month)
166 end
167
168 # Parses and checks the given `json` then returns an array of `Reclamation` instances.
169 private fun parse_recls(proc: RefundProcessor, json: JsonObject): Array[Reclamation] do
170 proc.check_key(json, "reclamations")
171 var res = new Array[Reclamation]
172 var recls = json["reclamations"]
173 if recls == null then
174 proc.die("Wrong type for `number` (expected JsonArray got null)")
175 abort
176 else if not recls isa JsonArray then
177 proc.die("Wrong type for `number` (expected JsonArray got {recls.class_name})")
178 abort
179 end
180 var i = 0
181 for obj in recls do
182 if obj == null then
183 proc.die("Wrong type for `reclamations#{i}` (expected JsonObject got null)")
184 abort
185 else if not obj isa JsonObject then
186 proc.die("Wrong type for `reclamations#{i}` " +
187 "(expected JsonObject got {obj.class_name})")
188 abort
189 end
190 var recl = new Reclamation.from_json(proc, obj)
191 if not month.has(recl.date) then
192 proc.die("Wrong `mois` for `soin` with id `{recl.care_id}`")
193 abort
194 end
195 if file.contract.care_by_id(recl.care_id) == null then
196 proc.die("Unknown `soin` with id `{recl.care_id}`")
197 abort
198 end
199 res.add recl
200 i += 1
201 end
202 return res
203 end
204 end
205
206 redef class ReclFile
207 # Inits `self` from the content of a JsonObject.
208 init from_json(proc: RefundProcessor, json: JsonObject) do
209 proc.check_key(json, "dossier")
210 var id = json["dossier"]
211 if id == null then
212 proc.die("Wrong type for `dossier` (expected String got null)")
213 abort
214 else if not id isa String then
215 proc.die("Wrong type for `dossier` (expected String got {id.class_name})")
216 abort
217 end
218 # Check format
219 parse_contract(proc, id)
220 parse_client(proc, id)
221 init(id)
222 end
223
224 # Tries to parse the contract from `file_id` string.
225 private fun parse_contract(proc: RefundProcessor, file_id: String) do
226 var kind = file_id.first.to_s
227 if not proc.check_format(kind, "^[A-E]\{1\}$") then
228 proc.die("Wrong contract (expected A, B, C, D or E got {kind})")
229 end
230 contract = contract_factory(proc, kind)
231 end
232
233 # Tries to parse the client number from the `file_id` string.
234 private fun parse_client(proc: RefundProcessor, file_id: String) do
235 var num = file_id.substring_from(1)
236 if not proc.check_format(num, "^[0-9]\{6\}$") then
237 proc.die("Wrong format for `number` (expected XXXXXX got {num})")
238 abort
239 end
240 client = new Client(num)
241 end
242 end
243
244 redef class ReclMonth
245 # Inits `self` from a `JsonObject`.
246 init from_json(proc: RefundProcessor, json: JsonObject) do
247 proc.check_key(json, "mois")
248 var month = json["mois"]
249 if month == null then
250 proc.die("Wrong type for `mois` (expected String got null)")
251 return
252 else if not month isa String then
253 proc.die("Wrong type for `mois` (expected String got {month.class_name})")
254 return
255 end
256 if not proc.check_format(month, "^[0-9]\{4\}-[0-9]\{2\}$") then
257 proc.die("Wrong format for `mois` (expected AAAA-MM got {month})")
258 return
259 end
260 from_string(proc, month)
261 end
262
263 # Inits `self` from a string representation formatted as `AAAA-MM`.
264 init from_string(proc: RefundProcessor, str: String) do
265 var parts = str.split("-")
266 var year = parts[0].to_i
267 var month = parts[1].to_i
268 if month < 1 or month > 12 then
269 proc.die("Wrong format for `mois` (expected AAAA-MM got {str})")
270 return
271 end
272 date = new ReclDate(year, month, 1)
273 init(date)
274 end
275 end
276
277 redef class ReclDate
278 # Inits `self` from a `JsonObject`.
279 #
280 # Dies if the `json` input is invalid.
281 init from_json(proc: RefundProcessor, json: JsonObject) do
282 proc.check_key(json, "date")
283 var date = json["date"]
284 if date == null then
285 proc.die("Wrong type for `date` (expected String got null)")
286 abort
287 else if not date isa String then
288 proc.die("Wrong type for `date` (expected String got {date.class_name})")
289 abort
290 end
291 if not proc.check_format(date, "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}$") then
292 proc.die("Wrong format for `date` (expected AAAA-MM-DD got {date})")
293 abort
294 end
295 from_string(proc, date)
296 end
297
298 # Inits `self` from its string representation formatted as `AAAA-MM`.
299 init from_string(proc: RefundProcessor, str: String) do
300 var parts = str.split("-")
301 year = parts[0].to_i
302 month = parts[1].to_i
303 day = parts[2].to_i
304 if month < 1 or month > 12 or day < 1 or day > 31 then
305 proc.die("Wrong format for `mois` (expected AAAA-MM got {str})")
306 abort
307 end
308 init(year, month, day)
309 end
310 end
311
312 redef class Reclamation
313 # Inits `self` from a `JsonObject`.
314 init from_json(proc: RefundProcessor, json: JsonObject) do
315 care_id = parse_care_id(proc, json)
316 date = new ReclDate.from_json(proc, json)
317 fees = parse_fees(proc, json)
318 init(care_id, date, fees)
319 end
320
321 # Inits `self` from its string representation formatted as `Int`.
322 private fun parse_care_id(proc: RefundProcessor, json: JsonObject): Int do
323 proc.check_key(json, "soin")
324 var id = json["soin"]
325 if id == null then
326 proc.die("Wrong type for `soin` (expected Int got null)")
327 abort
328 else if not id isa Int then
329 proc.die("Wrong type for `soin` (expected Int got {id.class_name})")
330 abort
331 end
332 return id
333 end
334
335 # Inits `self` from its string representation formatted as `0.00$`.
336 private fun parse_fees(proc: RefundProcessor, json: JsonObject): Dollar do
337 proc.check_key(json, "montant")
338 var fees = json["montant"]
339 if fees == null then
340 proc.die("Wrong type for `fees` (expected String got null)")
341 abort
342 else if not fees isa String then
343 proc.die("Wrong type for `fees` (expected String got {fees.class_name})")
344 abort
345 end
346 if not proc.check_format(fees, "^[0-9]+((\\.|\\,)[0-9]+)?\\$$") then
347 proc.die("Wrong format for `montant` (expected XX.XX$ got {fees})")
348 abort
349 end
350 return new Dollar.from_float(fees.basename("$").to_f)
351 end
352 end