pipeable 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +62 -97
- data/lib/pipeable/steps/amap.rb +10 -0
- data/lib/pipeable/steps/container.rb +1 -0
- data/pipeable.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c03f09d3e8268f1786faa5907fe4f9dbc4beaf02c874bec73b683fd1e4a51de
|
4
|
+
data.tar.gz: 29943bc0fed70a2661cee558e5a4d23090af2fe216ba085ec1ff00e3da435ea3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebaeb186fb3c672501fa3b5414b2d4fe381a45fa22004ae2f128b80abc15d012f0ff70befb900ad75d82b760af33fa4a35c7db5d3113e7f3f261782f01e7dd24
|
7
|
+
data.tar.gz: 41fe4361d2a0226ceeb8796d1edc9e6fd0e19bc00539c3d3c4e87c57d09a871818d051d7a8d881b294c0f3ea8e73f2575a0ead5dcaaf3d44dbbb0dd53065d749
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -21,7 +21,7 @@ toc::[]
|
|
21
21
|
|
22
22
|
== Features
|
23
23
|
|
24
|
-
* Built atop
|
24
|
+
* Built atop native {function_composition_link}.
|
25
25
|
* Adheres to the {railway_pattern_link}.
|
26
26
|
* Provides built-in and customizable domain-specific steps.
|
27
27
|
* Provides chainable _pipes_ which can be used to build more complex workflows.
|
@@ -131,7 +131,7 @@ pipe(input, *steps)
|
|
131
131
|
|
132
132
|
The first argument is your input which can be a Ruby primitive or a monad. Regardless, the input will be automatically wrapped as a `Success` -- but only if not a `Result` to begin with -- before passing to the first step. From there, all steps are _required_ to answer a monad in order to adhere to the {railway_pattern_link}.
|
133
133
|
|
134
|
-
Behind the scenes, the `#pipe` method is syntactic sugar
|
134
|
+
Behind the scenes, the `#pipe` method is syntactic sugar built atop {function_composition_link} which means if this code were to be rewritten:
|
135
135
|
|
136
136
|
[source,ruby]
|
137
137
|
----
|
@@ -141,7 +141,7 @@ pipe csv,
|
|
141
141
|
map { |item| "#{item[:book]}: #{item[:price]}" }
|
142
142
|
----
|
143
143
|
|
144
|
-
|
144
|
+
...then the above would look like the following (as rewritten in native Ruby):
|
145
145
|
|
146
146
|
[source,ruby]
|
147
147
|
----
|
@@ -152,27 +152,28 @@ Then the above would look like this using native Ruby:
|
|
152
152
|
).call Success(csv)
|
153
153
|
----
|
154
154
|
|
155
|
+
Visually, the pipe can be diagramed as follows:
|
156
|
+
|
157
|
+
image::https://alchemists.io/images/projects/pipeable/diagrams/pipe.png[A diagram of pipe steps,width=591,height=734,role=focal_point]
|
158
|
+
|
155
159
|
The problem with native function composition is that it reads backwards by passing input at the end of all sequential steps. With the `#pipe` method, you have the benefit of allowing your eyes to read from top to bottom while not having to type multiple _forward composition_ operators.
|
156
160
|
|
157
161
|
=== Steps
|
158
162
|
|
159
|
-
There are several ways to compose steps for your pipe. As long as all steps succeed, you'll get a successful response. Otherwise, the first step to fail will pass the failure down by skipping all subsequent steps (unless you dynamically turn the failure into a success).
|
163
|
+
There are several ways to compose steps for your pipe. As long as all steps succeed, you'll get a successful response. Otherwise, the first step to fail will pass the failure down by skipping all subsequent steps (unless you dynamically turn the failure into a success). Each step can be initialized and called:
|
160
164
|
|
161
|
-
|
165
|
+
* `+#initialize+`: Arguments vary per step but can be positional, keyword, and/or block arguments. This is how you _customize_ the behavior of each step.
|
166
|
+
* `+#call+`: Expects a {dry_monads_link} `Result` object as input. The output is either the same or new `Result` object for consumption by the next step in the pipe. Additionally, each step will either unwrap the `Result` or pass the `Result` through depending on the step's implementation (as detailed below).
|
162
167
|
|
163
168
|
==== Basic
|
164
169
|
|
165
170
|
The following are the basic (default) steps for building custom pipes for which you can mix and match within your own implementation.
|
166
171
|
|
167
|
-
=====
|
172
|
+
===== alt
|
168
173
|
|
169
174
|
Allows you to operate on a failure and produce either a success or another failure. This is a convenience wrapper to native {dry_monads_link} `#or` functionality.
|
170
175
|
|
171
|
-
|
172
|
-
|
173
|
-
Processes a failure only while expecting you to answer a success or failure.
|
174
|
-
|
175
|
-
*Example*
|
176
|
+
Accepts a failure while answering either a success or failure. Example:
|
176
177
|
|
177
178
|
[source,ruby]
|
178
179
|
----
|
@@ -181,15 +182,23 @@ pipe Failure("Danger!"), alt { Success "Resolved" } # Success "Re
|
|
181
182
|
pipe Failure("Danger!"), alt { |object| Failure "Big #{object}" } # Failure "Big Danger!"
|
182
183
|
----
|
183
184
|
|
184
|
-
=====
|
185
|
+
===== amap
|
185
186
|
|
186
|
-
Allows you to
|
187
|
+
Allows you to unwrap a failure, make a modification, and wrap the modification as a new failure. This is a convenience wrapper to native {dry_monads_link} `#alt_map` functionality.
|
187
188
|
|
188
|
-
|
189
|
+
Accepts and answers a failure. Example:
|
189
190
|
|
190
|
-
|
191
|
+
[source,ruby]
|
192
|
+
----
|
193
|
+
pipe Failure("Danger"), amap { |object| "#{object}!" } # Failure "Danger!"
|
194
|
+
pipe Success("Pass"), amap { |object| "#{object}!" } # Success "Pass"
|
195
|
+
----
|
191
196
|
|
192
|
-
|
197
|
+
===== as
|
198
|
+
|
199
|
+
Allows you to message an object as a different result. The first argument is the method but additional positional and/or keyword arguments can be passed along if the method accepts them.
|
200
|
+
|
201
|
+
Accepts and answers a success. Example:
|
193
202
|
|
194
203
|
[source,ruby]
|
195
204
|
----
|
@@ -198,15 +207,11 @@ pipe %i[a b c], as(:dig, 1) # Success :b
|
|
198
207
|
pipe Failure("Danger!"), as(:inspect) # Failure "Danger!"
|
199
208
|
----
|
200
209
|
|
201
|
-
=====
|
210
|
+
===== bind
|
202
211
|
|
203
212
|
Allows you to perform operations upon success only. You are then responsible for answering a success or failure accordingly. This is a convenience wrapper to native {dry_monads_link} `#bind` functionality.
|
204
213
|
|
205
|
-
|
206
|
-
|
207
|
-
Processes a success only while expecting you to answer a success or failure in return.
|
208
|
-
|
209
|
-
*Example*
|
214
|
+
Accepts a success while answering either a success or failure. Example:
|
210
215
|
|
211
216
|
[source,ruby]
|
212
217
|
----
|
@@ -215,15 +220,11 @@ pipe %i[a b c], bind { |object| Failure object } # Failure [
|
|
215
220
|
pipe Failure("Danger!"), bind { |object| Success object.join("-") } # Failure "Danger!"
|
216
221
|
----
|
217
222
|
|
218
|
-
=====
|
223
|
+
===== check
|
219
224
|
|
220
225
|
Allows you to check if an object matches the proof (with message). The first argument is your proof while the second argument is the message to send to your proof. A check only passes if the messaged object evaluates to `true` or `Success`. When successful, the object is passed through as a `Success`. When false, the object is passed through as a `Failure`.
|
221
226
|
|
222
|
-
|
223
|
-
|
224
|
-
Processes a success only while answering a success or failure depending on whether unwrapped object checks against the proof.
|
225
|
-
|
226
|
-
*Example*
|
227
|
+
Accepts a success while answering a success or failure depending on whether unwrapped object checks against the proof. Example:
|
227
228
|
|
228
229
|
[source,ruby]
|
229
230
|
----
|
@@ -232,15 +233,11 @@ pipe :a, check(%i[b c], :include?) # Failure :a
|
|
232
233
|
pipe Failure("Danger!"), check(%i[a b], :include?) # Failure "Danger!"
|
233
234
|
----
|
234
235
|
|
235
|
-
=====
|
236
|
+
===== fmap
|
236
237
|
|
237
|
-
Allows you to unwrap a success, make a modification, and
|
238
|
+
Allows you to unwrap a success, make a modification, and wrap the modification as a new success. This is a convenience wrapper to native {dry_monads_link} `#fmap` functionality.
|
238
239
|
|
239
|
-
|
240
|
-
|
241
|
-
Processes and answers a success only.
|
242
|
-
|
243
|
-
*Example*
|
240
|
+
Accepts and answers a success. Example:
|
244
241
|
|
245
242
|
[source,ruby]
|
246
243
|
----
|
@@ -248,15 +245,11 @@ pipe %i[a b c], fmap { |object| object.join "-" } # Success "a-b-c"
|
|
248
245
|
pipe Failure("Danger!"), fmap { |object| object.join "-" } # Failure "Danger!"
|
249
246
|
----
|
250
247
|
|
251
|
-
=====
|
252
|
-
|
253
|
-
Allows you to insert an element after an object (default behavior). This step wraps native link:https://rubyapi.org/o/array#method-i-insert[Array#insert] functionality. If the object is not an array, it will be cast as one. You can use the `:at` key to specify where you want insertion to happen. This step is most useful when needing to assemble arguments for passing to a subsequent step.
|
254
|
-
|
255
|
-
*I/O*
|
248
|
+
===== insert
|
256
249
|
|
257
|
-
|
250
|
+
Allows you to insert an element after an object (default behavior) as a single array. This step wraps native link:https://rubyapi.org/o/array#method-i-insert[Array#insert] functionality. If the object is not an array, it will be cast as one. You can use the `:at` key to specify where you want insertion to happen. This step is most useful when needing to assemble _positional_ arguments for passing as an array to a subsequent step.
|
258
251
|
|
259
|
-
|
252
|
+
Accepts and answers a success. Example:
|
260
253
|
|
261
254
|
[source,ruby]
|
262
255
|
----
|
@@ -266,15 +259,11 @@ pipe %i[a c], insert(:b, at: 1) # Success [:a, :b, :c]
|
|
266
259
|
pipe Failure("Danger!"), insert(:b) # Failure "Danger!"
|
267
260
|
----
|
268
261
|
|
269
|
-
=====
|
262
|
+
===== map
|
270
263
|
|
271
264
|
Allows you to map over an object (enumerable) by wrapping native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
|
272
265
|
|
273
|
-
|
274
|
-
|
275
|
-
Processes and answers a success only.
|
276
|
-
|
277
|
-
*Example*
|
266
|
+
Accepts and answers a success. Example:
|
278
267
|
|
279
268
|
[source,ruby]
|
280
269
|
----
|
@@ -282,15 +271,11 @@ pipe %i[a b c], map(&:inspect) # Success [":a", ":b", ":c"]
|
|
282
271
|
pipe Failure("Danger!"), map(&:inspect) # Failure "Danger!"
|
283
272
|
----
|
284
273
|
|
285
|
-
=====
|
286
|
-
|
287
|
-
Allows you to merge an object with additional attributes as a single hash. If the input is not a hash, then the object will be merged with `step` as the key. The default `step` key can be renamed to a different key by using the `:as` key. Like the _Insert_ step, this is most useful when assembling arguments and/or data for consumption by subsequent steps
|
274
|
+
===== merge
|
288
275
|
|
289
|
-
|
276
|
+
Allows you to merge an object with additional attributes as a single hash. This step wraps native link:https://rubyapi.org/o/hash#method-i-merge[Hash#merge] functionality. If the input is not a hash, then the object will be merged with `step` as the key. The default `step` key can be renamed to a different key by using the `:as` key. Like the _insert_ step, this step is most useful when assembling _keyword_ arguments and/or a hash for a subsequent steps.
|
290
277
|
|
291
|
-
|
292
|
-
|
293
|
-
*Example*
|
278
|
+
Accepts and answers a success. Example:
|
294
279
|
|
295
280
|
[source,ruby]
|
296
281
|
----
|
@@ -300,15 +285,11 @@ pipe "test", merge(as: :a, b: 2) # Success {a: "test", b: 2}
|
|
300
285
|
pipe Failure("Danger!"), merge(b: 2) # Failure "Danger!"
|
301
286
|
----
|
302
287
|
|
303
|
-
=====
|
288
|
+
===== tee
|
304
289
|
|
305
290
|
Allows you to run an operation and ignore the response while input is passed through as output. This behavior is similar in nature to the link:https://www.gnu.org/savannah-checkouts/gnu/gawk/manual/html_node/Tee-Program.html[tee] program in Bash.
|
306
291
|
|
307
|
-
|
308
|
-
|
309
|
-
Passes the result through while allowing you to execute arbitrary behavior.
|
310
|
-
|
311
|
-
*Example*
|
292
|
+
Accepts either a success or failure and passes the result through while allowing you to execute arbitrary behavior. Example:
|
312
293
|
|
313
294
|
[source,ruby]
|
314
295
|
----
|
@@ -323,15 +304,11 @@ pipe Failure("Danger!"), tee(Kernel, :puts, "Example.")
|
|
323
304
|
# Failure "Danger!"
|
324
305
|
----
|
325
306
|
|
326
|
-
=====
|
307
|
+
===== to
|
327
308
|
|
328
309
|
Allows you to delegate to an object which doesn't have a callable interface and may or may not answer a result. If the response is not a monad, it'll be automatically wrapped as a `Success`.
|
329
310
|
|
330
|
-
|
331
|
-
|
332
|
-
Processes a success only while sending the unwrapped object to the given object's corresponding method. The object is expected to answer either a plain Ruby object which will be automatically wrapped as a success or a {dry_monads_link} `Result`.
|
333
|
-
|
334
|
-
*Example*
|
311
|
+
Accepts a success while sending the unwrapped object to the given object's corresponding method. The object is expected to answer either a plain Ruby object which will be automatically wrapped as a success or a {dry_monads_link} `Result`. Example:
|
335
312
|
|
336
313
|
[source,ruby]
|
337
314
|
----
|
@@ -345,15 +322,11 @@ pipe({label: "Test"}, to(Model, :for)) # Success #<struct Model label="Test">
|
|
345
322
|
pipe Failure("Danger!"), to(Model, :for) # Failure "Danger!"
|
346
323
|
----
|
347
324
|
|
348
|
-
=====
|
325
|
+
===== try
|
349
326
|
|
350
327
|
Allows you to try an operation which may fail while catching any exceptions as a failure for further processing. You can catch a single exception by providing the exception as a single value or multiple exceptions as an array of values.
|
351
328
|
|
352
|
-
|
353
|
-
|
354
|
-
Processes and answers a success only if there are no exceptions. Otherwise, captures any error as a failure.
|
355
|
-
|
356
|
-
*Example*
|
329
|
+
Accepts and answers a success if there are no exceptions. Otherwise, captures any error as a failure. Example:
|
357
330
|
|
358
331
|
[source,ruby]
|
359
332
|
----
|
@@ -370,15 +343,11 @@ pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError)
|
|
370
343
|
# Failure "Danger!"
|
371
344
|
----
|
372
345
|
|
373
|
-
=====
|
346
|
+
===== use
|
374
347
|
|
375
348
|
Allows you to use another pipe to build a superpipe, use an object that adheres to the {command_pattern_link}, or any function which answers a {dry_monads_link} `Result` object. In other words, you can use _use_ any object which responds to `#call` that answers a {dry_monads_link} `Result` object. This is great for chaining multiple pipes together (i.e. superpipes).
|
376
349
|
|
377
|
-
|
378
|
-
|
379
|
-
Processes a success only while sending the unwrapped object to the command (or pipe) for further processing. A {dry_monads_link} `Result` is expected to be answered by the command.
|
380
|
-
|
381
|
-
*Example*
|
350
|
+
Accepts a success while sending the unwrapped object to the command (or pipe) for further processing. A {dry_monads_link} `Result` is expected to be answered by the command. Example:
|
382
351
|
|
383
352
|
[source,ruby]
|
384
353
|
----
|
@@ -388,19 +357,13 @@ pipe 3, use(function) # Success 9
|
|
388
357
|
pipe Failure("Danger!"), use(function) # Failure "Danger!"
|
389
358
|
----
|
390
359
|
|
391
|
-
=====
|
360
|
+
===== validate
|
392
361
|
|
393
362
|
Allows you to use an contract for validating an object. This is especially useful when using {dry_schema_link}, {dry_validation_link}, or any contract that responds to `#call` and answers a `Result`.
|
394
363
|
|
395
|
-
|
396
|
-
|
397
|
-
By default, the `:as` key's value is `nil``. Use `:to_h`, for example, as the value for automatic casting to a `Hash`. You can also pass in any value to the `:as` key which is a valid method that the contract's result will respond to.
|
364
|
+
By default, the `:as` key's value is `nil`. Use `:to_h`, for example, as the value for automatic casting to a `Hash`. You can also pass in any value to the `:as` key which is a valid method that the contract's result will respond to.
|
398
365
|
|
399
|
-
|
400
|
-
|
401
|
-
Processes a success only. A success will be rewrapped as a success if the `:as` keyword is supplied. Otherwise, any failure is immediately passed through.
|
402
|
-
|
403
|
-
*Example*
|
366
|
+
Accepts a success and rewraps as a success if the `:as` keyword is supplied. Otherwise, any failure is immediately passed through. Example:
|
404
367
|
|
405
368
|
[source,ruby]
|
406
369
|
----
|
@@ -416,6 +379,8 @@ pipe Failure("Danger!"), validate(schema)
|
|
416
379
|
# Failure "Danger!"
|
417
380
|
----
|
418
381
|
|
382
|
+
💡 Ensure you enable the {dry_monads_link} extension for {dry_schema_link} and/or {dry_validation_link} when using this step since this step expects the contract to respond to the `#to_monad` message.
|
383
|
+
|
419
384
|
==== Advanced
|
420
385
|
|
421
386
|
Several options are available should you need to advance beyond the basic steps. Each is described in detail below.
|
@@ -577,13 +542,13 @@ class Three
|
|
577
542
|
end
|
578
543
|
----
|
579
544
|
|
580
|
-
Notice, `One` and `Two` are
|
545
|
+
Notice, `One` and `Two` are normal pipeable objects with individual steps while `Three` injects both `One` and `Two` as dependencies and then subsequently pipes them together in the `#call` method via the `use` step. This is the power of a superpipe. ...and, yes, a superpipe can be an individual step in some other object. Turtles all the way down (or up). 😉
|
581
546
|
|
582
|
-
Again, the above is contrived but hopefully
|
547
|
+
Again, the above is contrived but hopefully illustrates how you can build more complex architectures from smaller pipes.
|
583
548
|
|
584
549
|
=== Containers
|
585
550
|
|
586
|
-
Should you not want the basic steps, need custom steps, or a hybrid of default and custom steps, you can define your own container and provide
|
551
|
+
Should you not want the basic steps, need custom steps, or a hybrid of default and custom steps, you can define your own container -- using the {containable_link} gem -- and provide the container as an argument to `.[]` when including pipeable behavior. Example:
|
587
552
|
|
588
553
|
[source,ruby]
|
589
554
|
----
|
@@ -636,7 +601,7 @@ The architecture of this gem is built on top of the following concepts and gems:
|
|
636
601
|
* {function_composition_link}: Made possible through the use of the `\#>>` and `#<<` methods on the link:https://rubyapi.org/3.1/o/method[Method] and link:https://rubyapi.org/3.1/o/proc[Proc] objects.
|
637
602
|
* {containable_link}: Allows related dependencies to be grouped together for injection as desired.
|
638
603
|
* {dry_monads_link}: Critical to ensuring the entire pipeline of steps adhere to the {railway_pattern_link} and leans heavily on the `Result` object.
|
639
|
-
* link:https://alchemists.io/projects/marameters[Marameters]: Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the
|
604
|
+
* link:https://alchemists.io/projects/marameters[Marameters]: Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the object's method parameters.
|
640
605
|
|
641
606
|
=== Style Guide
|
642
607
|
|
@@ -657,7 +622,7 @@ If you need to debug (i.e. {debug_link}) your pipe, use a lambda. Example:
|
|
657
622
|
----
|
658
623
|
pipe data,
|
659
624
|
check(/Book.+Price/, :match?),
|
660
|
-
-> result { binding.break },
|
625
|
+
-> result { binding.break; result }, # Breakpoint
|
661
626
|
:parse
|
662
627
|
----
|
663
628
|
|
@@ -671,12 +636,12 @@ The following might be of aid to as you implement your own pipes.
|
|
671
636
|
|
672
637
|
If you get a `TypeError: Step must be functionally composable and answer a monad`, it means:
|
673
638
|
|
674
|
-
. The step must be a `Proc`, `Method`, or
|
675
|
-
. The step doesn't answer a result monad (i.e. `Success
|
639
|
+
. The step must be a `Proc`, `Method`, or any object which responds to `\#>>`, `#<<`, and `#call`.
|
640
|
+
. The step doesn't answer a result monad (i.e. `Success object` or `Failure object`).
|
676
641
|
|
677
642
|
==== No Method Errors
|
678
643
|
|
679
|
-
If you get a `NoMethodError: undefined method
|
644
|
+
If you get a `NoMethodError: undefined method success?` exception, this might mean that you forgot to add a comma after one of your steps. Example:
|
680
645
|
|
681
646
|
[source,ruby]
|
682
647
|
----
|
@@ -687,7 +652,7 @@ pipe "https://www.wikipedia.org",
|
|
687
652
|
|
688
653
|
# Invalid
|
689
654
|
pipe "https://www.wikipedia.org",
|
690
|
-
to(client, :get)
|
655
|
+
to(client, :get) # Missing comma.
|
691
656
|
try(:parse, catch: HTTP::Error)
|
692
657
|
----
|
693
658
|
|
data/pipeable.gemspec
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pipeable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brooke Kuhlmann
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
|
36
36
|
gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2024-05-
|
38
|
+
date: 2024-05-12 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: containable
|
@@ -123,6 +123,7 @@ files:
|
|
123
123
|
- lib/pipeable/composable.rb
|
124
124
|
- lib/pipeable/pipe.rb
|
125
125
|
- lib/pipeable/steps/abstract.rb
|
126
|
+
- lib/pipeable/steps/amap.rb
|
126
127
|
- lib/pipeable/steps/as.rb
|
127
128
|
- lib/pipeable/steps/bind.rb
|
128
129
|
- lib/pipeable/steps/check.rb
|
metadata.gz.sig
CHANGED
Binary file
|