alpr-sdk 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,622 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Code generated by sdk/generator. DO NOT EDIT.
4
+
5
+ module Alpr
6
+ # Camera is a resource of Alpr.sh's API.
7
+ class Camera < Model
8
+ MEMBERS = %i[created_at enabled id location name protocol reading_area].freeze
9
+
10
+ # @return [String]
11
+ attr_reader :created_at
12
+
13
+ # @return [Boolean]
14
+ attr_reader :enabled
15
+
16
+ # @return [String]
17
+ attr_reader :id
18
+
19
+ # O endereço da câmera como texto; vazio se não foi informado.
20
+ # @return [String]
21
+ attr_reader :location
22
+
23
+ # @return [String]
24
+ attr_reader :name
25
+
26
+ # Como a câmera é lida. Nunca traz o endereço do stream, usuário nem senha.
27
+ # @return [String ("rtsp", "rtmp", "hls", "http", "hikconnect", "hikcloud", "rtmppush")]
28
+ attr_reader :protocol
29
+
30
+ # Nulo quando a câmera lê a imagem inteira.
31
+ # @return [ReadingArea, nil]
32
+ attr_reader :reading_area
33
+
34
+ def initialize(created_at:, enabled:, id:, location:, name:, protocol:, reading_area:)
35
+ @created_at = created_at
36
+ @enabled = enabled
37
+ @id = id
38
+ @location = location
39
+ @name = name
40
+ @protocol = protocol
41
+ @reading_area = reading_area
42
+ freeze
43
+ end
44
+
45
+ # Builds a Camera from the API's JSON.
46
+ def self.from_hash(data)
47
+ new(
48
+ created_at: data.fetch("createdAt"),
49
+ enabled: data.fetch("enabled"),
50
+ id: data.fetch("id"),
51
+ location: data.fetch("location"),
52
+ name: data.fetch("name"),
53
+ protocol: data.fetch("protocol"),
54
+ reading_area: data["readingArea"]&.then { |value| ReadingArea.from_hash(value) }
55
+ )
56
+ end
57
+ end
58
+
59
+ # DetectedPlate is a resource of Alpr.sh's API.
60
+ class DetectedPlate < Model
61
+ MEMBERS = %i[conf coordinate_bottom coordinate_left coordinate_right coordinate_top created_at plate_type time].freeze
62
+
63
+ # Confiança do detector, de 0 a 100, com uma casa.
64
+ # @return [Float]
65
+ attr_reader :conf
66
+
67
+ # @return [Integer]
68
+ attr_reader :coordinate_bottom
69
+
70
+ # @return [Integer]
71
+ attr_reader :coordinate_left
72
+
73
+ # @return [Integer]
74
+ attr_reader :coordinate_right
75
+
76
+ # @return [Integer]
77
+ attr_reader :coordinate_top
78
+
79
+ # @return [String]
80
+ attr_reader :created_at
81
+
82
+ # @return [String ("car")]
83
+ attr_reader :plate_type
84
+
85
+ # @return [String]
86
+ attr_reader :time
87
+
88
+ def initialize(conf:, coordinate_bottom:, coordinate_left:, coordinate_right:, coordinate_top:, created_at:, plate_type:, time:)
89
+ @conf = conf
90
+ @coordinate_bottom = coordinate_bottom
91
+ @coordinate_left = coordinate_left
92
+ @coordinate_right = coordinate_right
93
+ @coordinate_top = coordinate_top
94
+ @created_at = created_at
95
+ @plate_type = plate_type
96
+ @time = time
97
+ freeze
98
+ end
99
+
100
+ # Builds a DetectedPlate from the API's JSON.
101
+ def self.from_hash(data)
102
+ new(
103
+ conf: Float(data.fetch("conf")),
104
+ coordinate_bottom: data.fetch("coordinateBottom"),
105
+ coordinate_left: data.fetch("coordinateLeft"),
106
+ coordinate_right: data.fetch("coordinateRight"),
107
+ coordinate_top: data.fetch("coordinateTop"),
108
+ created_at: data.fetch("createdAt"),
109
+ plate_type: data.fetch("plateType"),
110
+ time: data.fetch("time")
111
+ )
112
+ end
113
+ end
114
+
115
+ # DetectionImageLink is a resource of Alpr.sh's API.
116
+ class DetectionImageLink < Model
117
+ MEMBERS = %i[detection_id expires_in image_url].freeze
118
+
119
+ # @return [String]
120
+ attr_reader :detection_id
121
+
122
+ # Segundos de validade do link (300).
123
+ # @return [Integer]
124
+ attr_reader :expires_in
125
+
126
+ # Endereço absoluto de GET /api/v1/detections/{id}/image/file, assinado; abra-o como veio.
127
+ # @return [String]
128
+ attr_reader :image_url
129
+
130
+ def initialize(detection_id:, expires_in:, image_url:)
131
+ @detection_id = detection_id
132
+ @expires_in = expires_in
133
+ @image_url = image_url
134
+ freeze
135
+ end
136
+
137
+ # Builds a DetectionImageLink from the API's JSON.
138
+ def self.from_hash(data)
139
+ new(
140
+ detection_id: data.fetch("detectionId"),
141
+ expires_in: data.fetch("expiresIn"),
142
+ image_url: data.fetch("imageUrl")
143
+ )
144
+ end
145
+ end
146
+
147
+ # PhotoDetection is a resource of Alpr.sh's API.
148
+ class PhotoDetection < Model
149
+ MEMBERS = %i[image_height image_width plates processing_time_ms].freeze
150
+
151
+ # @return [Integer]
152
+ attr_reader :image_height
153
+
154
+ # @return [Integer]
155
+ attr_reader :image_width
156
+
157
+ # Onde há placa, sem o texto; o detector não filtra pelo formato brasileiro. Sem placa, [].
158
+ # @return [Array<DetectedPlate>]
159
+ attr_reader :plates
160
+
161
+ # @return [Integer]
162
+ attr_reader :processing_time_ms
163
+
164
+ def initialize(image_height:, image_width:, plates:, processing_time_ms:)
165
+ @image_height = image_height
166
+ @image_width = image_width
167
+ @plates = plates
168
+ @processing_time_ms = processing_time_ms
169
+ freeze
170
+ end
171
+
172
+ # Builds a PhotoDetection from the API's JSON.
173
+ def self.from_hash(data)
174
+ new(
175
+ image_height: data.fetch("imageHeight"),
176
+ image_width: data.fetch("imageWidth"),
177
+ plates: data.fetch("plates").map { |item| DetectedPlate.from_hash(item) },
178
+ processing_time_ms: data.fetch("processingTimeMs")
179
+ )
180
+ end
181
+ end
182
+
183
+ # PhotoRecognition is a resource of Alpr.sh's API.
184
+ class PhotoRecognition < Model
185
+ MEMBERS = %i[credits_remaining image_height image_width plates processing_time_ms].freeze
186
+
187
+ # Créditos que sobraram depois desta leitura.
188
+ # @return [Integer]
189
+ attr_reader :credits_remaining
190
+
191
+ # @return [Integer]
192
+ attr_reader :image_height
193
+
194
+ # @return [Integer]
195
+ attr_reader :image_width
196
+
197
+ # Só as placas válidas no Brasil (ABC1234 ou ABC1D23); sem placa, [].
198
+ # @return [Array<RecognizedPlate>]
199
+ attr_reader :plates
200
+
201
+ # @return [Integer]
202
+ attr_reader :processing_time_ms
203
+
204
+ def initialize(credits_remaining:, image_height:, image_width:, plates:, processing_time_ms:)
205
+ @credits_remaining = credits_remaining
206
+ @image_height = image_height
207
+ @image_width = image_width
208
+ @plates = plates
209
+ @processing_time_ms = processing_time_ms
210
+ freeze
211
+ end
212
+
213
+ # Builds a PhotoRecognition from the API's JSON.
214
+ def self.from_hash(data)
215
+ new(
216
+ credits_remaining: data.fetch("creditsRemaining"),
217
+ image_height: data.fetch("imageHeight"),
218
+ image_width: data.fetch("imageWidth"),
219
+ plates: data.fetch("plates").map { |item| RecognizedPlate.from_hash(item) },
220
+ processing_time_ms: data.fetch("processingTimeMs")
221
+ )
222
+ end
223
+ end
224
+
225
+ # PlateReading is a resource of Alpr.sh's API.
226
+ class PlateReading < Model
227
+ MEMBERS = %i[confidence credits_remaining detection_id latency_ms plate].freeze
228
+
229
+ # Confiança da leitura, de 0 a 1 com quatro casas (nas outras rotas conf vai de 0 a 100).
230
+ # @return [Float]
231
+ attr_reader :confidence
232
+
233
+ # Créditos que sobraram depois desta leitura.
234
+ # @return [Integer]
235
+ attr_reader :credits_remaining
236
+
237
+ # Hoje sempre nulo: a leitura de uma placa recortada não é guardada.
238
+ # @return [String, nil]
239
+ attr_reader :detection_id
240
+
241
+ # @return [Integer]
242
+ attr_reader :latency_ms
243
+
244
+ # O texto lido, sem hífen, maiúsculo; não é validado pelo formato brasileiro.
245
+ # @return [String]
246
+ attr_reader :plate
247
+
248
+ def initialize(confidence:, credits_remaining:, detection_id:, latency_ms:, plate:)
249
+ @confidence = confidence
250
+ @credits_remaining = credits_remaining
251
+ @detection_id = detection_id
252
+ @latency_ms = latency_ms
253
+ @plate = plate
254
+ freeze
255
+ end
256
+
257
+ # Builds a PlateReading from the API's JSON.
258
+ def self.from_hash(data)
259
+ new(
260
+ confidence: Float(data.fetch("confidence")),
261
+ credits_remaining: data.fetch("creditsRemaining"),
262
+ detection_id: data["detectionId"],
263
+ latency_ms: data.fetch("latencyMs"),
264
+ plate: data.fetch("plate")
265
+ )
266
+ end
267
+ end
268
+
269
+ # ReadingArea is a resource of Alpr.sh's API.
270
+ class ReadingArea < Model
271
+ MEMBERS = %i[box points].freeze
272
+
273
+ # A caixa envolvente, em frações da imagem (0 a 1); é o que o motor lê.
274
+ # @return [ReadingAreaBox]
275
+ attr_reader :box
276
+
277
+ # Os cantos do polígono, de 3 a 16 pares [x, y] em frações da imagem; um retângulo traz os quatro cantos. Placas fora dele são ignoradas.
278
+ # @return [Array<Array<Float>>]
279
+ attr_reader :points
280
+
281
+ def initialize(box:, points:)
282
+ @box = box
283
+ @points = points
284
+ freeze
285
+ end
286
+
287
+ # Builds a ReadingArea from the API's JSON.
288
+ def self.from_hash(data)
289
+ new(
290
+ box: ReadingAreaBox.from_hash(data.fetch("box")),
291
+ points: data.fetch("points").map { |item| item.map { |item_| Float(item_) } }
292
+ )
293
+ end
294
+ end
295
+
296
+ # A caixa envolvente, em frações da imagem (0 a 1); é o que o motor lê.
297
+ class ReadingAreaBox < Model
298
+ MEMBERS = %i[height width x y].freeze
299
+
300
+ # @return [Float]
301
+ attr_reader :height
302
+
303
+ # @return [Float]
304
+ attr_reader :width
305
+
306
+ # @return [Float]
307
+ attr_reader :x
308
+
309
+ # @return [Float]
310
+ attr_reader :y
311
+
312
+ def initialize(height:, width:, x:, y:)
313
+ @height = height
314
+ @width = width
315
+ @x = x
316
+ @y = y
317
+ freeze
318
+ end
319
+
320
+ # Builds a ReadingAreaBox from the API's JSON.
321
+ def self.from_hash(data)
322
+ new(
323
+ height: Float(data.fetch("height")),
324
+ width: Float(data.fetch("width")),
325
+ x: Float(data.fetch("x")),
326
+ y: Float(data.fetch("y"))
327
+ )
328
+ end
329
+ end
330
+
331
+ # Recognition is a resource of Alpr.sh's API.
332
+ class Recognition < Model
333
+ MEMBERS = %i[camera_id captured_at conf id plate source].freeze
334
+
335
+ # Nulo numa leitura feita pela API por foto.
336
+ # @return [String, nil]
337
+ attr_reader :camera_id
338
+
339
+ # Com o deslocamento do fuso do Alpr.sh.
340
+ # @return [String]
341
+ attr_reader :captured_at
342
+
343
+ # De 0 a 100, com uma casa.
344
+ # @return [Float]
345
+ attr_reader :conf
346
+
347
+ # @return [String]
348
+ attr_reader :id
349
+
350
+ # @return [String]
351
+ attr_reader :plate
352
+
353
+ # camera: lida por uma câmera; api: enviada por foto.
354
+ # @return [String ("camera", "api")]
355
+ attr_reader :source
356
+
357
+ def initialize(camera_id:, captured_at:, conf:, id:, plate:, source:)
358
+ @camera_id = camera_id
359
+ @captured_at = captured_at
360
+ @conf = conf
361
+ @id = id
362
+ @plate = plate
363
+ @source = source
364
+ freeze
365
+ end
366
+
367
+ # Builds a Recognition from the API's JSON.
368
+ def self.from_hash(data)
369
+ new(
370
+ camera_id: data["cameraId"],
371
+ captured_at: data.fetch("capturedAt"),
372
+ conf: Float(data.fetch("conf")),
373
+ id: data.fetch("id"),
374
+ plate: data.fetch("plate"),
375
+ source: data.fetch("source")
376
+ )
377
+ end
378
+ end
379
+
380
+ # RecognizedPlate is a resource of Alpr.sh's API.
381
+ class RecognizedPlate < Model
382
+ MEMBERS = %i[char_confs conf coordinate_bottom coordinate_left coordinate_right coordinate_top created_at plate plate_type time].freeze
383
+
384
+ # Confiança de cada caractere, de 0 a 1 com duas casas, separadas por ponto e vírgula ("0.99;0.98").
385
+ # @return [String]
386
+ attr_reader :char_confs
387
+
388
+ # Confiança da placa, de 0 a 100, com uma casa.
389
+ # @return [Float]
390
+ attr_reader :conf
391
+
392
+ # Pixels da foto enviada.
393
+ # @return [Integer]
394
+ attr_reader :coordinate_bottom
395
+
396
+ # @return [Integer]
397
+ attr_reader :coordinate_left
398
+
399
+ # @return [Integer]
400
+ attr_reader :coordinate_right
401
+
402
+ # @return [Integer]
403
+ attr_reader :coordinate_top
404
+
405
+ # Igual a time.
406
+ # @return [String]
407
+ attr_reader :created_at
408
+
409
+ # Sete caracteres, maiúsculos: ABC1234 ou ABC1D23 (Mercosul).
410
+ # @return [String]
411
+ attr_reader :plate
412
+
413
+ # Sempre car; a API não informa país nem formato.
414
+ # @return [String ("car")]
415
+ attr_reader :plate_type
416
+
417
+ # Instante da leitura, com o deslocamento do fuso do Alpr.sh.
418
+ # @return [String]
419
+ attr_reader :time
420
+
421
+ def initialize(char_confs:, conf:, coordinate_bottom:, coordinate_left:, coordinate_right:, coordinate_top:, created_at:, plate:, plate_type:, time:)
422
+ @char_confs = char_confs
423
+ @conf = conf
424
+ @coordinate_bottom = coordinate_bottom
425
+ @coordinate_left = coordinate_left
426
+ @coordinate_right = coordinate_right
427
+ @coordinate_top = coordinate_top
428
+ @created_at = created_at
429
+ @plate = plate
430
+ @plate_type = plate_type
431
+ @time = time
432
+ freeze
433
+ end
434
+
435
+ # Builds a RecognizedPlate from the API's JSON.
436
+ def self.from_hash(data)
437
+ new(
438
+ char_confs: data.fetch("charConfs"),
439
+ conf: Float(data.fetch("conf")),
440
+ coordinate_bottom: data.fetch("coordinateBottom"),
441
+ coordinate_left: data.fetch("coordinateLeft"),
442
+ coordinate_right: data.fetch("coordinateRight"),
443
+ coordinate_top: data.fetch("coordinateTop"),
444
+ created_at: data.fetch("createdAt"),
445
+ plate: data.fetch("plate"),
446
+ plate_type: data.fetch("plateType"),
447
+ time: data.fetch("time")
448
+ )
449
+ end
450
+ end
451
+
452
+ # WebhookAlertTriggered is a resource of Alpr.sh's API.
453
+ class WebhookAlertTriggered < Model
454
+ MEMBERS = %i[camera_id captured_at confidence event id plate recognition_id rule_id rule_name skip tenant_id].freeze
455
+
456
+ # Ausente se a leitura não veio de uma câmera.
457
+ # @return [String, nil]
458
+ attr_reader :camera_id
459
+
460
+ # @return [String]
461
+ attr_reader :captured_at
462
+
463
+ # De 0 a 1.
464
+ # @return [Float]
465
+ attr_reader :confidence
466
+
467
+ # @return [String ("alert.triggered")]
468
+ attr_reader :event
469
+
470
+ # O id do disparo.
471
+ # @return [String]
472
+ attr_reader :id
473
+
474
+ # @return [String]
475
+ attr_reader :plate
476
+
477
+ # A leitura que disparou o alerta.
478
+ # @return [String]
479
+ attr_reader :recognition_id
480
+
481
+ # @return [String]
482
+ attr_reader :rule_id
483
+
484
+ # @return [String]
485
+ attr_reader :rule_name
486
+
487
+ # Os destinos que as listas de permitidos e de bloqueados excluíram deste disparo (internal, channel:<id>, partner:<id>). Presente só quando algum foi excluído; é informação, não há o que fazer com ela.
488
+ # @return [Array<String>, nil]
489
+ attr_reader :skip
490
+
491
+ # @return [String]
492
+ attr_reader :tenant_id
493
+
494
+ def initialize(camera_id: nil, captured_at:, confidence:, event:, id:, plate:, recognition_id:, rule_id:, rule_name:, skip: nil, tenant_id:)
495
+ @camera_id = camera_id
496
+ @captured_at = captured_at
497
+ @confidence = confidence
498
+ @event = event
499
+ @id = id
500
+ @plate = plate
501
+ @recognition_id = recognition_id
502
+ @rule_id = rule_id
503
+ @rule_name = rule_name
504
+ @skip = skip
505
+ @tenant_id = tenant_id
506
+ freeze
507
+ end
508
+
509
+ # Builds a WebhookAlertTriggered from the API's JSON.
510
+ def self.from_hash(data)
511
+ new(
512
+ camera_id: data["camera_id"],
513
+ captured_at: data.fetch("captured_at"),
514
+ confidence: Float(data.fetch("confidence")),
515
+ event: data.fetch("event"),
516
+ id: data.fetch("id"),
517
+ plate: data.fetch("plate"),
518
+ recognition_id: data.fetch("recognition_id"),
519
+ rule_id: data.fetch("rule_id"),
520
+ rule_name: data.fetch("rule_name"),
521
+ skip: data["skip"],
522
+ tenant_id: data.fetch("tenant_id")
523
+ )
524
+ end
525
+ end
526
+
527
+ # WebhookRecognitionCreated is a resource of Alpr.sh's API.
528
+ class WebhookRecognitionCreated < Model
529
+ MEMBERS = %i[camera_id captured_at confidence event id plate source tenant_id].freeze
530
+
531
+ # Ausente numa leitura enviada por foto.
532
+ # @return [String, nil]
533
+ attr_reader :camera_id
534
+
535
+ # UTC, com fração de segundo.
536
+ # @return [String]
537
+ attr_reader :captured_at
538
+
539
+ # De 0 a 1 (na API de chave, conf vai de 0 a 100).
540
+ # @return [Float]
541
+ attr_reader :confidence
542
+
543
+ # @return [String ("recognition.created")]
544
+ attr_reader :event
545
+
546
+ # O id da leitura; é o mesmo id de GET /api/v1/recognitions e de GET /api/v1/detections/{id}/image.
547
+ # @return [String]
548
+ attr_reader :id
549
+
550
+ # @return [String]
551
+ attr_reader :plate
552
+
553
+ # camera: lida por uma câmera; api: enviada por foto.
554
+ # @return [String ("camera", "api")]
555
+ attr_reader :source
556
+
557
+ # O espaço de trabalho dono da leitura.
558
+ # @return [String]
559
+ attr_reader :tenant_id
560
+
561
+ def initialize(camera_id: nil, captured_at:, confidence:, event:, id:, plate:, source:, tenant_id:)
562
+ @camera_id = camera_id
563
+ @captured_at = captured_at
564
+ @confidence = confidence
565
+ @event = event
566
+ @id = id
567
+ @plate = plate
568
+ @source = source
569
+ @tenant_id = tenant_id
570
+ freeze
571
+ end
572
+
573
+ # Builds a WebhookRecognitionCreated from the API's JSON.
574
+ def self.from_hash(data)
575
+ new(
576
+ camera_id: data["camera_id"],
577
+ captured_at: data.fetch("captured_at"),
578
+ confidence: Float(data.fetch("confidence")),
579
+ event: data.fetch("event"),
580
+ id: data.fetch("id"),
581
+ plate: data.fetch("plate"),
582
+ source: data.fetch("source"),
583
+ tenant_id: data.fetch("tenant_id")
584
+ )
585
+ end
586
+ end
587
+
588
+ # WebhookTest is a resource of Alpr.sh's API.
589
+ class WebhookTest < Model
590
+ MEMBERS = %i[event id sent_at tenant_id].freeze
591
+
592
+ # @return [String ("webhook.test")]
593
+ attr_reader :event
594
+
595
+ # @return [String]
596
+ attr_reader :id
597
+
598
+ # @return [String]
599
+ attr_reader :sent_at
600
+
601
+ # @return [String]
602
+ attr_reader :tenant_id
603
+
604
+ def initialize(event:, id:, sent_at:, tenant_id:)
605
+ @event = event
606
+ @id = id
607
+ @sent_at = sent_at
608
+ @tenant_id = tenant_id
609
+ freeze
610
+ end
611
+
612
+ # Builds a WebhookTest from the API's JSON.
613
+ def self.from_hash(data)
614
+ new(
615
+ event: data.fetch("event"),
616
+ id: data.fetch("id"),
617
+ sent_at: data.fetch("sent_at"),
618
+ tenant_id: data.fetch("tenant_id")
619
+ )
620
+ end
621
+ end
622
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Alpr
6
+ # A multipart/form-data body, written by hand: one file and the text fields
7
+ # beside it.
8
+ class Form
9
+ # The Content-Type and file name of a photo, told by its first bytes: JPEG
10
+ # and PNG are what the API reads, and anything else goes as bytes for the
11
+ # API to refuse.
12
+ JPEG_MAGIC = "\xFF\xD8\xFF".b.freeze
13
+ PNG_MAGIC = "\x89PNG\r\n\x1A\n".b.freeze
14
+
15
+ def self.image_type(data)
16
+ head = data.byteslice(0, 8).to_s.b
17
+ return ["image/jpeg", "photo.jpg"] if head.start_with?(JPEG_MAGIC)
18
+ return ["image/png", "photo.png"] if head.start_with?(PNG_MAGIC)
19
+
20
+ ["application/octet-stream", "photo"]
21
+ end
22
+
23
+ # A number, boolean or string as the text of a field: the shortest decimal
24
+ # form, 0.5 and 1, not 0.50 or 1.0.
25
+ def self.text_of(value)
26
+ return value.to_s unless value.is_a?(Float) && value.finite?
27
+
28
+ value == value.floor && value.abs < 1e15 ? value.to_i.to_s : value.to_s.sub(/\.0e/, "e")
29
+ end
30
+
31
+ def initialize
32
+ @parts = []
33
+ end
34
+
35
+ # Adds a text field.
36
+ def text(name, value)
37
+ @parts << [:text, name.to_s, Form.text_of(value).b]
38
+ self
39
+ end
40
+
41
+ # Adds the photo, named and typed by what its first bytes say it is.
42
+ def file(name, data)
43
+ raise ArgumentError, "alpr: #{name} is the bytes of a photo (a String), not #{data.class}" unless data.is_a?(String)
44
+
45
+ @parts << [:file, name.to_s, data.b]
46
+ self
47
+ end
48
+
49
+ # The body, as bytes.
50
+ def body
51
+ encode.first
52
+ end
53
+
54
+ # The Content-Type header, boundary included.
55
+ def content_type
56
+ "multipart/form-data; boundary=#{encode.last}"
57
+ end
58
+
59
+ private
60
+
61
+ # Writes the body once, with a boundary no part contains.
62
+ def encode
63
+ @encoded ||= begin
64
+ boundary = nil
65
+ loop do
66
+ boundary = "alpr-#{SecureRandom.hex(16)}".b
67
+ break unless @parts.any? { |_, _, data| data.include?(boundary) }
68
+ end
69
+ out = "".b
70
+ @parts.each do |kind, name, data|
71
+ out << "--".b << boundary << "\r\n".b
72
+ if kind == :file
73
+ type, filename = Form.image_type(data)
74
+ out << %(Content-Disposition: form-data; name="#{quote(name)}"; filename="#{filename}"\r\n).b
75
+ out << "Content-Type: #{type}\r\n".b
76
+ else
77
+ out << %(Content-Disposition: form-data; name="#{quote(name)}"\r\n).b
78
+ end
79
+ out << "\r\n".b << data << "\r\n".b
80
+ end
81
+ out << "--".b << boundary << "--\r\n".b
82
+ [out, boundary]
83
+ end
84
+ end
85
+
86
+ def quote(name)
87
+ name.gsub(/["\r\n]/) { |char| format("%%%02X", char.ord) }
88
+ end
89
+ end
90
+ end