ializer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b39a084832bc15cc9d722c7269b3200b9e4a6af8c4318ea06390f91acf6a27d5
4
+ data.tar.gz: '0355388f6e02ac9f8c156706230e172d36b3698d0f22df9f5a90ebf0f121a0b4'
5
+ SHA512:
6
+ metadata.gz: 0b4123ae5e65828108879cf191062ef3f05e980bd1fab81b1b51c262d576ea69664cc2d2a2e75d902049fdf52b33d9c25d26021a90d1c0fda7ae228b65315a25
7
+ data.tar.gz: ceab7b40078424c4bb23634e059a99ce7fd157bee574e1e7fa6e9202a64d9482a0458acf29d978349ca38f38e71ac58a316acf9e9a3e10a19b48a4ef70998b6a
@@ -0,0 +1,45 @@
1
+ name: Ruby
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+
9
+ strategy:
10
+ matrix:
11
+ ruby: [ '2.5.x', '2.6.x' ]
12
+
13
+ name: Ruby ${{ matrix.ruby }}
14
+ steps:
15
+ - uses: actions/checkout@v1
16
+ - name: Set up Ruby ${{ matrix.ruby }}
17
+ uses: actions/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby }}
20
+
21
+ - name: Install bundler
22
+ run: |
23
+ gem install bundler
24
+
25
+ - name: Cache bundled gems
26
+ uses: actions/cache@v1
27
+ with:
28
+ path: vendor/bundle
29
+ key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/ializer.gemspec') }}
30
+ restore-keys: |
31
+ ${{ runner.os }}-${{ matrix.ruby }}-${{ hashFiles('**/ializer.gemspec') }}
32
+ ${{ runner.os }}-${{ matrix.ruby }}-
33
+
34
+ - name: Bundle
35
+ run: |
36
+ bundle config path vendor/bundle
37
+ bundle install --jobs 4 --retry 3
38
+
39
+ - name: RSpec
40
+ run: |
41
+ bundle exec rspec
42
+
43
+ - name: Rubocop
44
+ run: |
45
+ bundle exec rubocop -c .rubocop.yml
@@ -0,0 +1,39 @@
1
+ name: RubyGems Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]+.[0-9]+.[0-9]+*'
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@master
14
+
15
+ - name: Set up Ruby 2.6
16
+ uses: actions/setup-ruby@v1
17
+ with:
18
+ version: 2.6.x
19
+
20
+ - name: Set Credentials
21
+ run: |
22
+ mkdir -p $HOME/.gem
23
+ touch $HOME/.gem/credentials
24
+ chmod 0600 $HOME/.gem/credentials
25
+ printf -- "---\n:github: Bearer ${GITHUB_TOKEN}\n:rubygems: Bearer ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
26
+ env:
27
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
28
+ RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
29
+
30
+ - name: Publish to GitHub Packages
31
+ run: |
32
+ export OWNER=$( echo ${{ github.repository }} | cut -d "/" -f 1 )
33
+ gem build *.gemspec
34
+ gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
35
+
36
+ - name: Publish to RubyGems
37
+ run: |
38
+ gem build *.gemspec
39
+ gem push --KEY rubygems *.gem
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
data/.mdlrc ADDED
@@ -0,0 +1 @@
1
+ rules "~MD013"
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion:
5
+ 2.6
6
+
7
+ Layout/LineLength:
8
+ Max: 120
9
+
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - 'spec/**/*.rb'
13
+
14
+ Style/Documentation:
15
+ Enabled: false
16
+
17
+ RSpec/MultipleExpectations:
18
+ Enabled: false
19
+
20
+ RSpec/ExampleLength:
21
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.1
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2019 Jeremy Steinberg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,557 @@
1
+ [![Build](https://github.com/jsteinberg/ializer/workflows/Ruby/badge.svg)](https://github.com/jsteinberg/ializer/actions)
2
+
3
+ # {De | Ser} Ializer
4
+
5
+ A fast serializer/deserializer for Ruby Objects.
6
+
7
+ ## Table of Contents
8
+
9
+ * [Design Goals](#design-goals)
10
+ * [Installation](#installation)
11
+ * [Usage](#usage)
12
+ * [Configuration](#configuration)
13
+ * [Model Definitions](#model-definitions)
14
+ * [Serializer Definitions](#serializer-definitions)
15
+ * [DeSerializer Definitions](#deserializer-definitions)
16
+ * [Object Serialization](#object-serialization)
17
+ * [Object Deserialization](#object-deserialization)
18
+ * [Attributes](#attributes)
19
+ * [Nested Attributes](#nested-attributes)
20
+ * [Attribute Types](#attribute-types)
21
+ * [Serialization Context](#serialization-context)
22
+ * [Conditional Attributes](#conditional-attributes)
23
+ * [Attribute Sharing](#attribute-sharing)
24
+ * [Key Transforms](#key-transforms)
25
+ * [Thread Safety](#thread-safety)
26
+ * [Performance Comparison](#performance-comparison)
27
+ * [Contributing](#contributing)
28
+
29
+ ## Design Goals
30
+
31
+ * Simple Singular DSL for defining object-to-data and data-to-object mappings
32
+ * Support for nested object relationships
33
+ * Isolate serialization/parsing code from object to not pollute method space
34
+ * speed
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem 'ializer'
42
+ ```
43
+
44
+ Execute:
45
+
46
+ ```bash
47
+ bundle install
48
+ ```
49
+
50
+ Require:
51
+
52
+ ```ruby
53
+ require 'ializer'
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Configuration
59
+
60
+ ```ruby
61
+ Ializer.setup do |config|
62
+ config.key_transform = :dasherize # change serailized key names
63
+ # or
64
+ config.key_transformer = ->(key) {
65
+ key.lowercase.undsercore + '1'
66
+ }
67
+ config.warn_on_default = true # outputs a warning to STDOUT(puts) if DefaultDeSer is used
68
+ config.raise_on_default = false # raises an exception if the DefaultDeSer is used
69
+ end
70
+
71
+ ```
72
+
73
+ For more information, see [Key Transforms](#key-transforms) and [Attribute Types](#attribute-types) sections.
74
+
75
+ ### Model Definitions
76
+
77
+ ```ruby
78
+ class Order
79
+ attr_accessor :id, :created_at, :items, :customer
80
+
81
+ def initialize(attr = {})
82
+ @id = attr[:id]
83
+ @created_at = attr[:created_at]
84
+ @items = attr[:items] || []
85
+ end
86
+ end
87
+
88
+ class OrderItem
89
+ attr_accessor :name, :price, :in_stock
90
+
91
+ def initialize(attr = {})
92
+ @name = attr[:name]
93
+ @price = attr[:price]
94
+ @in_stock = attr[:in_stock]
95
+ end
96
+ end
97
+
98
+ class Customer
99
+ attr_accessor :name, :email
100
+
101
+ def initialize(attr = {})
102
+ @name = attr[:name]
103
+ @email = attr[:email]
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### Serializer Definition
109
+
110
+ ```ruby
111
+ class OrderDeSer < Ser::Ializer
112
+ integer :id
113
+ timestamp :created_at
114
+
115
+ nested :items, deser: OrderItemDeSer
116
+ nested :customer, deser: CustomerDeSer
117
+ end
118
+
119
+ class OrderItemDeSer < Ser::Ializer
120
+ string :name
121
+ decimal :price
122
+ boolean :in_stock
123
+ end
124
+
125
+ class CustomerDeSer < Ser::Ializer
126
+ string :name
127
+ string :email
128
+ end
129
+ ```
130
+
131
+ ### DeSerializer Definition
132
+
133
+ `De::Ser::Ializers` can deserialize from JSON and serialize to JSON. If you only need serialization capabilities, you can inherit from, `Ser::Italizer` instead.
134
+
135
+ ```ruby
136
+ class OrderDeSer < De::Ser::Ializer
137
+ integer :id
138
+ timestamp :created_at
139
+
140
+ nested :items, deser: OrderItemDeSer, model_class: OrderItem
141
+ nested :customer, deser: CustomerDeSer, model_class: Customer
142
+ end
143
+
144
+ class OrderItemDeSer < De::Ser::Ializer
145
+ string :name
146
+ decimal :price
147
+ boolean :in_stock
148
+ end
149
+
150
+ class CustomerDeSer < De::Ser::Ializer
151
+ string :name
152
+ string :email
153
+ end
154
+ ```
155
+
156
+ ### Sample Object
157
+
158
+ ```ruby
159
+ order = Order.new(id: 4, created_at: Time.now, items: [])
160
+ order.items << OrderItem.new(name: 'Baseball', price: BigDecimal('4.99'), in_stock: true)
161
+ order.items << OrderItem.new(name: 'Football', price: BigDecimal('14.99'), in_stock: false)
162
+ order.customer = Customer.new(name: 'Bowser', email: 'bowser@example.net')
163
+ ```
164
+
165
+ ### Object Serialization
166
+
167
+ #### Return a hash
168
+
169
+ ```ruby
170
+ data = OrderDeSer.serialize(order)
171
+ ```
172
+
173
+ #### Return Serialized JSON
174
+
175
+ Ializer relies on the [`MultiJson`](https://github.com/intridea/multi_json) gem for json serialization/parsing
176
+
177
+ ```ruby
178
+ json_string = OrderDeser.serialize_json(order)
179
+ ```
180
+
181
+ #### Serialized Output
182
+
183
+ ```json
184
+ {
185
+ "id": 4,
186
+ "created-at": "2019-12-01T00:00:00.000-06:00",
187
+ "items": [
188
+ {
189
+ "name": "Baseball",
190
+ "decimal": "4.99",
191
+ "in-stock": true
192
+ },
193
+ {
194
+ "name": "Football",
195
+ "decimal": "14.99",
196
+ "in-stock": false
197
+ }
198
+ ],
199
+ "customer": {
200
+ "name": "Bowser",
201
+ "email": "bowser@example.net"
202
+ }
203
+ }
204
+ ```
205
+
206
+ #### Serialize a collection
207
+
208
+ ```ruby
209
+ data = OrderDeSer.serialize([order, order2])
210
+ ```
211
+
212
+ #### Serialized Collection Output
213
+
214
+ ```json
215
+ [
216
+ {
217
+ "id": 3,
218
+ ...
219
+ },
220
+ {
221
+ "id": 4,
222
+ ...
223
+ }
224
+ ]
225
+ ```
226
+
227
+ ### Object Deserialization
228
+
229
+ **Note:** Objects that are parsed must have a zero-argument initializer (ie: Object.new)
230
+
231
+ #### Parsing a hash
232
+
233
+ ```ruby
234
+ model = OrderDeSer.parse(data, Order)
235
+
236
+ => #<Order:0x00007f9e44aabd80 @id=4, @created_at=Sun, 01 Dec 2019 00:00:00 -0600, @items=[#<OrderItem:0x00007f9e44aab6f0 @name="Baseball", @in_stock=true, @price=0.499e1>, #<OrderItem:0x00007f9e44aab628 @name="Football", @in_stock=false, @price=0.1499e2>], @customer=#<Customer:0x00007f9e44aab498 @name="Bowser", @email="bowser@example.net">>
237
+ ```
238
+
239
+ ## Attributes
240
+
241
+ Attributes are defined in `ializer` using the `property` method.
242
+
243
+ By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized:
244
+
245
+ ```ruby
246
+ class Customer < De::Ser::Ializer
247
+ property :id, type: :integer
248
+ property :name, type: String
249
+ end
250
+ ```
251
+
252
+ Custom typed methods also exist to provide a cleaner DSL.
253
+
254
+ ```ruby
255
+ class Customer < De::Ser::Ializer
256
+ integer :id
257
+ string :name
258
+ end
259
+ ```
260
+
261
+ ### Nested Attributes
262
+
263
+ `ializer` was built for serialization and parsing of nested objects. You can create a nested object via the `property` method or a specialized `nested` method.
264
+
265
+ The `nested` method allows you to define a deser inline.
266
+
267
+ ```ruby
268
+ class OrderDeSer < De::Ser::Ializer
269
+ integer :id
270
+ timestamp :created_at
271
+
272
+ nested :items, deser: OrderItemDeSer, model_class: OrderItem
273
+ # OR
274
+ property :items, deser: OrderItemDeSer, model_classx: OrderItem
275
+
276
+ nested :customer, model_class: Customer do
277
+ string :name
278
+ string :email
279
+ end
280
+ end
281
+ ```
282
+
283
+ The `property` method **DOES NOT** allow you to define a deser inline, but instead allows you to override the value of the field.
284
+
285
+ ```ruby
286
+ class OrderDeSer < De::Ser::Ializer
287
+ integer :id
288
+ property :items, deser: OrderItemDeSer, model_class: OrderItem do |object, _context|
289
+ object.items.select(&:should_display?)
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### Attribute Types
295
+
296
+ The following types are included with `ializer`
297
+
298
+ | Type | method alias | mappings |
299
+ |------------|--------------|----------------------------|
300
+ | BigDecimal | `decimal()` | :BigDecimal, :decimal |
301
+ | Boolean | `boolean()` | :Boolean, :boolean |
302
+ | Date | `date()` | Date, :date |
303
+ | Integer | `integer()` | Integer, :integer |
304
+ | Float | `float()` | Float, :float |
305
+ | Time | `millis()` | :Millis, :millis |
306
+ | String | `string()` | String, :millis |
307
+ | Symbol | `symbol()` | Symbol, :symbol, :sym |
308
+ | Time | `timestamp()`| Time, DateTime, :timestamp |
309
+ | Default | `default()` | :default |
310
+
311
+ **Note: Default just uses the current value of the field and will only properly deserialize if it is a standard json value type(number, string, boolean).**
312
+
313
+ #### Default Attribute Configuration Options
314
+
315
+ There are a few settings for dealing with the `DefaultDeSer`.
316
+
317
+ ```ruby
318
+ Ializer.setup do |config|
319
+ config.warn_on_default = true # outputs a warning to STDOUT(puts) if DefaultDeSer is used
320
+ config.raise_on_default = false # raises an exception if the DefaultDeSer is used
321
+ end
322
+ ```
323
+
324
+ Since `De::Ser::Ializer`s are configured on load, raising an exception should halt the application from starting(instead of silently failing later). By default a warning is logged to `STDOUT`.
325
+
326
+ #### Registering Custom Attribute types
327
+
328
+ You can register your own types or override the provided types. A custom attribute type DeSer must implement the following methods. When registering, you must register with the base `Ser::Ializer` class.
329
+
330
+ ```ruby
331
+ Ser::Ializer.register(method_name, deser, *mappings)
332
+ ```
333
+
334
+ ```ruby
335
+ class CustomDeSer
336
+ def self.serialize(value, _context = nil)
337
+ "#{value}_custom"
338
+ end
339
+
340
+ def self.parse(value)
341
+ value.split("_")[0]
342
+ end
343
+ end
344
+
345
+ Ser::Ializer.register(:custom, CustomDeSer, :custom)
346
+ ```
347
+
348
+ Then you can use them as follows:
349
+
350
+ ```ruby
351
+ class Customer < De::Ser::Ializer
352
+ integer :id
353
+ string :name
354
+ custom :custom_prop
355
+ # or
356
+ property :custom_prop, type: :custom
357
+ end
358
+ ```
359
+
360
+ To override the provided type desers, you do the following:
361
+
362
+ ```ruby
363
+ class MyTimeDeSer
364
+ def self.serialize(value, _context = nil)
365
+ value.to_my_favorite_time_serialization_format
366
+ end
367
+
368
+ def self.parse(value)
369
+ Time.parse_my_favorite_time_serialization_format(value)
370
+ end
371
+ end
372
+
373
+ Ser::Ializer.register('timestamp', MyTimeDeSer, Time, DateTime, :timestamp)
374
+ ```
375
+
376
+ Custom attributes that must be serialized but do not exist on the model can be declared using Ruby block syntax:
377
+
378
+ ```ruby
379
+ class Customer < De::Ser::Ializer
380
+ string :full_name do |object, _context|
381
+ "#{object.first_name} (#{object.last_name})"
382
+ end
383
+ end
384
+ ```
385
+
386
+ The block syntax can also be used to override the property on the object:
387
+
388
+ ```ruby
389
+ class Customer < De::Ser::Ializer
390
+ string :name do |object, _context|
391
+ "#{object.name} Part 2"
392
+ end
393
+ end
394
+ ```
395
+
396
+ You can also override the property on an object with a specially named method:
397
+
398
+ ```ruby
399
+ class Customer < De::Ser::Ializer
400
+ string :name
401
+
402
+ def self.name(object, _context) # overrides :name attribute
403
+ "#{object.name} Part 2"
404
+ end
405
+ end
406
+ ```
407
+
408
+ Attributes can also use a different name by passing the original method or accessor with a proc shortcut:
409
+
410
+ ```ruby
411
+ class Customer < De::Ser::Ializer
412
+ string :name, key: 'customer-name' # Note: an explicitly set key will not be transformed by the configured key_transformer
413
+ end
414
+ ```
415
+
416
+ ### Serialization Context
417
+
418
+ In some cases a `Ser::Ializer` might require more information than what is available on the record. A context object can be passed to serialization and used however necessary.
419
+
420
+ ```ruby
421
+ class CustomerSerializer < Ser::Ializer
422
+ integer :id
423
+ string :name
424
+
425
+ string :phone_number do |object, context|
426
+ if context.admin?
427
+ object.phone_number
428
+ else
429
+ object.phone_number.last(4)
430
+ end
431
+ end
432
+ end
433
+
434
+ # ...
435
+
436
+ CustomerDeSer.serialize(order, current_user)
437
+ ```
438
+
439
+ ### Conditional Attributes
440
+
441
+ Conditional attributes can be defined by passing a Proc to the `if` key on the `property` method. Return `truthy` if the attribute should be serialized, and `falsey` if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
442
+
443
+ ```ruby
444
+ class CustomerSerializer < Ser::Ializer
445
+ integer :id
446
+ string :name
447
+
448
+ string :phone_number if: ->(object, context) { context.admin? }
449
+ end
450
+
451
+ # ...
452
+ CustomerDeSer.serialize(order, current_user)
453
+ ```
454
+
455
+ **Note: instead of a Proc, any object that responds to call with arity 2 can be passed to `:if`.**
456
+
457
+ ```ruby
458
+ class AdminChecker
459
+ def self.admin?(_object, context)
460
+ context.admin?
461
+ end
462
+ end
463
+
464
+ class CustomerSerializer < Ser::Ializer
465
+ integer :id
466
+ string :name
467
+
468
+ string :phone_number if: AdminChecker.method(:admin?)
469
+ end
470
+
471
+ # ...
472
+ CustomerDeSer.serialize(order, current_user)
473
+ ```
474
+
475
+ ### Attribute Sharing
476
+
477
+ There are a couple of ways to share attributes from different desers.
478
+
479
+ #### Inheritance
480
+
481
+ ```ruby
482
+ class SimpleUserDeSer < De::Ser::Ializer
483
+ integer :id
484
+ string :username
485
+ end
486
+
487
+ class UserDeSer < SimpleUserDeSer
488
+ string :email
489
+ end
490
+ ```
491
+
492
+ #### Composition
493
+
494
+ ```ruby
495
+ class BaseApiDeSer < De::Ser::Ializer
496
+ integer :id
497
+ timestamp :created_at
498
+ end
499
+
500
+ class UserDeSer < De::Ser::Ializer
501
+ with BaseApiDeSer
502
+ string :email
503
+ end
504
+ ```
505
+
506
+ **Note:** Because of scoping, including a deser using `with` will not include any method overrides
507
+
508
+ ```ruby
509
+ class BaseApiDeSer < De::Ser::Ializer
510
+ integer :id
511
+ timestamp :created_at
512
+
513
+ def self.created_at(object, context)
514
+ # NOT INCLUDED IN UserDeSer. UserDeSer will call `.created_at` on any serialization target
515
+ end
516
+ end
517
+
518
+ class UserDeSer < De::Ser::Ializer
519
+ with BaseApiDeSer
520
+ string :email
521
+ end
522
+ ```
523
+
524
+ For more examples check the [`spec/support/deser`](https://github.com/jsteinberg/ializer/tree/master/spec/support/deser) folder.
525
+
526
+ ### Key Transforms
527
+
528
+ By default `ializer` uses object field names as the key name. You can override this setting by either specifying a string method for transforms or providing a proc for manual transformation.
529
+
530
+ **Note:** `key_transformer` will override any value set as the `key_transform`
531
+
532
+ ```ruby
533
+ Ializer.setup do |config|
534
+ config.key_transform = :dasherize
535
+ end
536
+
537
+ # or
538
+
539
+ Ializer.setup do |config|
540
+ config.key_transformer = ->(key) {
541
+ key.lowercase.undsercore + '1'
542
+ }
543
+ end
544
+
545
+ ```
546
+
547
+ ### Thread Safety
548
+
549
+ Defining of desers is not thread safe. As long as defitions are preloaded then thread safety is not a concern. **Note: because of this you should not create desers at runtime**
550
+
551
+ ## Performance Comparison
552
+
553
+ TODO
554
+
555
+ ## Contributing
556
+
557
+ TODO