smart_types 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile.lock +33 -32
  4. data/README.md +161 -16
  5. data/lib/smart_core/types.rb +2 -0
  6. data/lib/smart_core/types/errors.rb +12 -0
  7. data/lib/smart_core/types/primitive.rb +49 -4
  8. data/lib/smart_core/types/primitive/caster.rb +5 -2
  9. data/lib/smart_core/types/primitive/checker.rb +5 -2
  10. data/lib/smart_core/types/primitive/factory.rb +64 -6
  11. data/lib/smart_core/types/primitive/factory/definition_context.rb +37 -11
  12. data/lib/smart_core/types/primitive/factory/runtime_type_builder.rb +53 -0
  13. data/lib/smart_core/types/primitive/invariant_control.rb +6 -3
  14. data/lib/smart_core/types/primitive/invariant_control/chain.rb +5 -2
  15. data/lib/smart_core/types/primitive/invariant_control/single.rb +5 -2
  16. data/lib/smart_core/types/primitive/mult_factory.rb +40 -6
  17. data/lib/smart_core/types/primitive/mult_factory/definition_context.rb +23 -1
  18. data/lib/smart_core/types/primitive/mult_validator.rb +8 -0
  19. data/lib/smart_core/types/primitive/nilable_factory.rb +10 -3
  20. data/lib/smart_core/types/primitive/runtime_attributes_checker.rb +77 -0
  21. data/lib/smart_core/types/primitive/sum_factory.rb +40 -6
  22. data/lib/smart_core/types/primitive/sum_factory/definition_context.rb +23 -1
  23. data/lib/smart_core/types/primitive/sum_validator.rb +18 -2
  24. data/lib/smart_core/types/primitive/undefined_caster.rb +4 -1
  25. data/lib/smart_core/types/primitive/validator.rb +16 -7
  26. data/lib/smart_core/types/protocol.rb +7 -0
  27. data/lib/smart_core/types/protocol/instance_of.rb +19 -0
  28. data/lib/smart_core/types/value/array.rb +3 -0
  29. data/lib/smart_core/types/value/big_decimal.rb +4 -1
  30. data/lib/smart_core/types/value/boolean.rb +3 -0
  31. data/lib/smart_core/types/value/class.rb +3 -0
  32. data/lib/smart_core/types/value/date.rb +3 -0
  33. data/lib/smart_core/types/value/date_time.rb +3 -0
  34. data/lib/smart_core/types/value/enumerable.rb +1 -1
  35. data/lib/smart_core/types/value/float.rb +3 -0
  36. data/lib/smart_core/types/value/hash.rb +3 -0
  37. data/lib/smart_core/types/value/integer.rb +3 -0
  38. data/lib/smart_core/types/value/module.rb +3 -0
  39. data/lib/smart_core/types/value/numeric.rb +3 -0
  40. data/lib/smart_core/types/value/proc.rb +3 -0
  41. data/lib/smart_core/types/value/set.rb +11 -3
  42. data/lib/smart_core/types/value/string.rb +3 -0
  43. data/lib/smart_core/types/value/string_io.rb +2 -0
  44. data/lib/smart_core/types/value/symbol.rb +3 -0
  45. data/lib/smart_core/types/value/text.rb +6 -4
  46. data/lib/smart_core/types/value/time.rb +3 -0
  47. data/lib/smart_core/types/value/time_based.rb +8 -5
  48. data/lib/smart_core/types/variadic.rb +7 -0
  49. data/lib/smart_core/types/variadic/tuple.rb +23 -0
  50. data/lib/smart_core/types/version.rb +2 -2
  51. data/smart_types.gemspec +3 -3
  52. metadata +15 -10
  53. data/.travis.yml +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5f0c7f2b54ceaa7fe20e85d7d280b64b8310eb5f4fa802d11052074e6b10f96
4
- data.tar.gz: 021c3c6194fb4f54246f4a44a7c548ce9da0790a66b95e0a060547e73efeaf3f
3
+ metadata.gz: fdcbf29c692c77b4968df1ad7cbc629164de818da1859fc871c776b8e94152b1
4
+ data.tar.gz: 3fe9ffe11f8d84b11a3b274a59c1d826a2d8a64eb9c289c1bf5166690d379f31
5
5
  SHA512:
6
- metadata.gz: 6e2fc86bcea2292570d2c51e9b95ead6ecc30f8251db75a61fda088c1a84bdb15cea65fdc478728869fb5e5e56b7cddb774938cae38c60e4dee70197e6c1c7f3
7
- data.tar.gz: c7b27e2f70f3f8eac851f63365b3287a2c7aeb8f7f999679daa715d5ff7de93b32bd6204ea67f6af2c2ea1e5ccd569027051674c9266c62ff7d425c55692619a
6
+ metadata.gz: d8dd2d8a3e397c02caf8a6b32347105b2f2b860c3847a8f08d5713d5e1791845aa8a598a21b3dc8be63c526e63d323349e144366836518a74409c39904140eef
7
+ data.tar.gz: 5ead029c27b2b4aae1a6b509e0e268f92cf85fc34c207f5af76b4b79cdb72a494bb61d16a5e50d979c61a4d6b161983b45bd013207ca047f4b5852601c7bd088
@@ -1,6 +1,31 @@
1
1
  # Changelog
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## [0.3.0] - 2020-12-22
5
+ ### Added
6
+ - Extended **Type Definition API**: support for **runtime attributes**:
7
+ - Type checkers, type casters and type invariants now receives runtime attributes (you can omit these);
8
+ - Type definitioning extended with `runtime_attribute_checker`-checker definition (runtime attributes validator);
9
+ - Types with incorrect runtime attributes will raise `SmartCore::Types::IncorrectRuntimeAttributesError` exception;
10
+ - Types which has no support for runtime attributes will raise `SmartCore::Types::RuntimeAttributesUnsupportedError` excpetion;
11
+ - All types by default has a method alias (`()`) which does not allow runtime attributes (for example: `SmartCore::Types::Value::String` has
12
+ a runtime-based alias `SmartCore::Types::Value::String()` which does not accept any attribute
13
+ (`SmartCore::Types::Value::String('test')` will raise `SmartCore::Types::RuntimeAttributesUnsupportedError` respectively));
14
+ - Extended Internal **Type Development API**:
15
+ - all types has a reference to it's type category;
16
+ - Brand new `SmartCore::Types::Protocol` type category and new types:
17
+ - `SmartCore::Types::Protocol::InstanceOf` (runtime-based type);
18
+ - Brand new `SmartCore::Types::Variadic` type category and new types:
19
+ - `SmartCore::Types::Variadic::Tuple` (runtime-based type);
20
+ - New types of `SmartCore::Types::Value` category:
21
+ - `SmartCore::Types::Value::Set` (based on `Set` type with a support for type casting);
22
+ - Support for BasicObject values inside type checkers that can not be checked correctly via `#is_a?/#kind_of?` before;
23
+
24
+ ### Changed
25
+ - Updated development dependencies;
26
+ - Drop `Travis CI` (TODO: migrate to `Github Actions`);
27
+ - **Ruby@2.4** is no longer supported;
28
+
4
29
  ## [0.2.0] - 2020-11-21
5
30
  ### Added
6
31
  - Brand new **Type invariant API**:
@@ -1,34 +1,34 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_types (0.2.0)
5
- smart_engine (~> 0.7)
4
+ smart_types (0.3.0)
5
+ smart_engine (~> 0.10)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activesupport (6.0.3.4)
10
+ activesupport (6.1.0)
11
11
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (>= 0.7, < 2)
13
- minitest (~> 5.1)
14
- tzinfo (~> 1.1)
15
- zeitwerk (~> 2.2, >= 2.2.2)
16
- armitage-rubocop (1.3.1.1)
17
- rubocop (= 1.3.1)
18
- rubocop-performance (= 1.9.0)
19
- rubocop-rails (= 2.8.1)
12
+ i18n (>= 1.6, < 2)
13
+ minitest (>= 5.1)
14
+ tzinfo (~> 2.0)
15
+ zeitwerk (~> 2.3)
16
+ armitage-rubocop (1.6.1)
17
+ rubocop (= 1.6.1)
18
+ rubocop-performance (= 1.9.1)
19
+ rubocop-rails (= 2.9.0)
20
20
  rubocop-rake (= 0.5.1)
21
- rubocop-rspec (= 2.0.0)
21
+ rubocop-rspec (= 2.0.1)
22
22
  ast (2.4.1)
23
23
  coderay (1.1.3)
24
24
  concurrent-ruby (1.1.7)
25
25
  diff-lcs (1.4.4)
26
- docile (1.3.2)
26
+ docile (1.3.3)
27
27
  i18n (1.8.5)
28
28
  concurrent-ruby (~> 1.0)
29
29
  method_source (1.0.0)
30
30
  minitest (5.14.2)
31
- parallel (1.20.0)
31
+ parallel (1.20.1)
32
32
  parser (2.7.2.0)
33
33
  ast (~> 2.4.1)
34
34
  pry (0.13.1)
@@ -36,8 +36,8 @@ GEM
36
36
  method_source (~> 1.0)
37
37
  rack (2.2.3)
38
38
  rainbow (3.0.0)
39
- rake (13.0.1)
40
- regexp_parser (1.8.2)
39
+ rake (13.0.3)
40
+ regexp_parser (2.0.1)
41
41
  rexml (3.2.4)
42
42
  rspec (3.10.0)
43
43
  rspec-core (~> 3.10.0)
@@ -52,51 +52,52 @@ GEM
52
52
  diff-lcs (>= 1.2.0, < 2.0)
53
53
  rspec-support (~> 3.10.0)
54
54
  rspec-support (3.10.0)
55
- rubocop (1.3.1)
55
+ rubocop (1.6.1)
56
56
  parallel (~> 1.10)
57
57
  parser (>= 2.7.1.5)
58
58
  rainbow (>= 2.2.2, < 4.0)
59
- regexp_parser (>= 1.8)
59
+ regexp_parser (>= 1.8, < 3.0)
60
60
  rexml
61
- rubocop-ast (>= 1.1.1)
61
+ rubocop-ast (>= 1.2.0, < 2.0)
62
62
  ruby-progressbar (~> 1.7)
63
63
  unicode-display_width (>= 1.4.0, < 2.0)
64
- rubocop-ast (1.1.1)
64
+ rubocop-ast (1.3.0)
65
65
  parser (>= 2.7.1.5)
66
- rubocop-performance (1.9.0)
66
+ rubocop-performance (1.9.1)
67
67
  rubocop (>= 0.90.0, < 2.0)
68
68
  rubocop-ast (>= 0.4.0)
69
- rubocop-rails (2.8.1)
69
+ rubocop-rails (2.9.0)
70
70
  activesupport (>= 4.2.0)
71
71
  rack (>= 1.1)
72
- rubocop (>= 0.87.0)
72
+ rubocop (>= 0.90.0, < 2.0)
73
73
  rubocop-rake (0.5.1)
74
74
  rubocop
75
- rubocop-rspec (2.0.0)
75
+ rubocop-rspec (2.0.1)
76
76
  rubocop (~> 1.0)
77
77
  rubocop-ast (>= 1.1.0)
78
78
  ruby-progressbar (1.10.1)
79
- simplecov (0.19.1)
79
+ simplecov (0.20.0)
80
80
  docile (~> 1.1)
81
81
  simplecov-html (~> 0.11)
82
+ simplecov_json_formatter (~> 0.1)
82
83
  simplecov-html (0.12.3)
83
- smart_engine (0.8.0)
84
- thread_safe (0.3.6)
85
- tzinfo (1.2.8)
86
- thread_safe (~> 0.1)
84
+ simplecov_json_formatter (0.1.2)
85
+ smart_engine (0.10.0)
86
+ tzinfo (2.0.4)
87
+ concurrent-ruby (~> 1.0)
87
88
  unicode-display_width (1.7.0)
88
- zeitwerk (2.4.1)
89
+ zeitwerk (2.4.2)
89
90
 
90
91
  PLATFORMS
91
92
  ruby
92
93
 
93
94
  DEPENDENCIES
94
- armitage-rubocop (~> 1.3)
95
+ armitage-rubocop (~> 1.6)
95
96
  bundler (~> 2.1)
96
97
  pry (~> 0.13)
97
98
  rake (~> 13.0)
98
99
  rspec (~> 3.10)
99
- simplecov (~> 0.19)
100
+ simplecov (~> 0.20)
100
101
  smart_types!
101
102
 
102
103
  BUNDLED WITH
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # SmartCore::Types &middot; [![Gem Version](https://badge.fury.io/rb/smart_types.svg)](https://badge.fury.io/rb/smart_types) [![Build Status](https://travis-ci.org/smart-rb/smart_types.svg?branch=master)](https://travis-ci.org/smart-rb/smart_types)
1
+ # SmartCore::Types &middot; [![Gem Version](https://badge.fury.io/rb/smart_types.svg)](https://badge.fury.io/rb/smart_types)
2
2
 
3
3
  > A set of objects that acts like types (type checking and type casting) with a support for basic type algebra.
4
4
 
@@ -24,6 +24,25 @@ require 'smart_core/types'
24
24
 
25
25
  ---
26
26
 
27
+ ## Usage
28
+
29
+ - [Type interface](#type-interface)
30
+ - [Basic type algebra](#basic-type-algebra)
31
+ - [Supported types](#supported-types)
32
+ - [Nilable types](#nilable-types)
33
+ - [Custom type definition](#custom-type-definition)
34
+ - [Primitive type definition](#primitive-type-definition)
35
+ - [With type invariants](#with-type-invariants)
36
+ - [Type validation](#type-validation)
37
+ - [Type casting](#type-casting)
38
+ - [Roadmap](#roadmap)
39
+ - [Contributing](#contributing)
40
+ - [Build](#build)
41
+ - [License](#license)
42
+ - [Authors](#authors)
43
+
44
+ ---
45
+
27
46
  ## Type Interface
28
47
 
29
48
  ```ruby
@@ -37,6 +56,21 @@ type3 = type1 | type2
37
56
  type4 = type1 & type2
38
57
  ```
39
58
 
59
+ Types with runtime:
60
+
61
+ ```ruby
62
+ # get a type object with a custom runtime (instances of String or Symbol):
63
+ type = SmartCore::Types::Protocol::InstanceOf(::String, ::Symbol)
64
+ type.valid?(:test) # => true
65
+ type.valid?('test') # => true
66
+ type.valid?(123.456) # => false
67
+
68
+ # another type object with a custom runtime (tuple (String, Integer, Time)):
69
+ type = SmartCore::Types::Variadic::Tuple(::String, ::Integer, ::DateTime)
70
+ type.valid?(['test', 1, DateTime.new]) # => true
71
+ type.valid?([:test, 2]) # => false
72
+ ```
73
+
40
74
  ## Supported types
41
75
 
42
76
  - Primitives
@@ -53,6 +87,7 @@ SmartCore::Types::Value::Numeric
53
87
  SmartCore::Types::Value::BigDecimal
54
88
  SmartCore::Types::Value::Boolean
55
89
  SmartCore::Types::Value::Array
90
+ SmartCore::Types::Value::Set
56
91
  SmartCore::Types::Value::Hash
57
92
  SmartCore::Types::Value::Proc
58
93
  SmartCore::Types::Value::Class
@@ -63,6 +98,31 @@ SmartCore::Types::Value::Date
63
98
  SmartCore::Types::Value::TimeBased
64
99
  ```
65
100
 
101
+ - Protocols:
102
+
103
+ ```ruby
104
+ SmartCore::Types::Protocol::InstanceOf
105
+ ```
106
+
107
+ ```ruby
108
+ # examples (SmartCore::Types::Protocol::InstanceOf):
109
+ SmartCore::Types::Protocol::InstanceOf(::Integer) # only integer
110
+ SmartCore::Types::Protocol::InstanceOf(::String, ::Symbol) # string or symbol
111
+ SmartCore::Types::Protocol::InstanceOf(::Time, ::DateTime, ::Date) # time or datetime or date
112
+ ```
113
+
114
+ - Variadic:
115
+
116
+ ```ruby
117
+ SmartCore::Types::Variadic::Tuple
118
+ ```
119
+
120
+ ```ruby
121
+ # examples (SmartCore::Types::Variadic::Tuple):
122
+ SmartCore::Types::Variadic::Tuple(::String, ::Integer, ::Time) # array with signature [<string>, <integer>, <time>]
123
+ SmartCore::Types::Variadic::Tuple(::Symbol, ::Float) # array with signature [<symbol>, <float>]
124
+ ```
125
+
66
126
  ---
67
127
 
68
128
  ## Nilable types
@@ -107,41 +167,56 @@ Invariant checking is a special validation layer (see [#type validation](#type-v
107
167
 
108
168
  # example:
109
169
  SmartCore::Types::Value.define_type(:String) do |type|
110
- type.define_checker do |value|
170
+ type.define_checker do |value, runtime_attrs| # runtime attributes are optional
111
171
  value.is_a?(::String)
112
172
  end
113
173
 
114
- type.define_caster do |value|
174
+ type.define_caster do |value, runtime_attrs| # runtime attributes are optional
115
175
  value.to_s
116
176
  end
117
177
  end
178
+
179
+ # get a type object:
180
+ SmartCore::Types::Value::String
181
+ # --- or ---
182
+ SmartCore::Types::Value::String() # without runtime attributes
183
+ # --- or ---
184
+ SmartCore::Types::Value::String('some_attr', :another_attr) # with runtime attributes
185
+
186
+ # work with type object: see documentation below
118
187
  ```
119
188
 
120
189
  #### With type invariants
121
190
 
122
191
  ```ruby
123
192
  SmartCore::Types::Value.define_type(:String) do |type|
124
- type.define_checker do |value|
193
+ type.define_checker do |value, runtime_attrs|
125
194
  value.is_a?(::String)
126
195
  end
127
196
 
128
- type.define_caster do |value|
197
+ type.define_caster do |value, runtime_attrs|
129
198
  value.to_s
130
199
  end
131
200
 
132
201
  # NOTE:
133
202
  # invariant defined out from chain does not depends on other invariants
134
- type.invariant(:uncensored_content) do |value|
203
+ type.invariant(:uncensored_content) do |value, runtime_attrs|
135
204
  !value.include?('uncensored_word')
136
205
  end
137
206
 
138
- type.invariant(:filled) do |value|
207
+ type.invariant(:filled) do |value, runtime_attrs|
139
208
  value != ''
140
209
  end
141
210
 
142
211
  type.invariant_chain(:password) do
143
- invariant(:should_present) { |value| value != '' }
144
- invariant(:should_have_numbers) { |value| v.match?(/[0-9]+/) }
212
+ invariant(:should_present) do |value, runtime_attrs|
213
+ value != ''
214
+ end
215
+
216
+ invariant(:should_have_numbers) do |value, runtime_attrs|
217
+ v.match?(/[0-9]+/)
218
+ end
219
+
145
220
  # NOTE:
146
221
  # inside a chain each next invariant invokation
147
222
  # depends on previous successful invariant check
@@ -190,6 +265,36 @@ Validation result object interface:
190
265
  - for single invariant: `TypeName.invariant_name`;
191
266
  - `#checked_value` (the same as `#value`) - checked value :)
192
267
 
268
+ ---
269
+
270
+ Imagine that we have `String` type like this:
271
+
272
+ ```ruby
273
+ SmartCore::Types::Value.define_type(:String) do |type|
274
+ type.define_checker do |value|
275
+ value.is_a?(::String)
276
+ end
277
+
278
+ type.define_caster do |value|
279
+ value.to_s
280
+ end
281
+
282
+ type.invariant(:uncensored_content) do |value|
283
+ !value.include?('uncensored_word')
284
+ end
285
+
286
+ type.invariant(:filled) do |value|
287
+ value != ''
288
+ end
289
+
290
+ type.invariant_chain(:password) do
291
+ invariant(:should_present) { |value| value != '' }
292
+ invariant(:should_have_numbers) { |value| v.match?(/[0-9]+/) }
293
+ end
294
+ end
295
+ ```
296
+
297
+ Validation interface and usage:
193
298
 
194
299
  ```ruby
195
300
  SmartCore::Types::Value::String.valid?('test123') # => true
@@ -231,7 +336,7 @@ SmartCore::Types::Value::String.validate!('test') # => SmartCore::Types::TypeErr
231
336
 
232
337
  ## Type casting
233
338
 
234
- ```
339
+ ```ruby
235
340
  SmartCore::Types::Value::String.cast(123) # => "123"
236
341
  SmartCore::Types::Value::Float.cast('55') # => 55.0
237
342
  ```
@@ -257,6 +362,10 @@ SmartCore::Types::Value::CryptoString = SmartCore::Types::Value::NumberdString &
257
362
 
258
363
  ## Roadmap
259
364
 
365
+ - migrate to `Github Actions`;
366
+
367
+ - support for `block`-attribute in runtime attributes;
368
+
260
369
  - type configuration:
261
370
 
262
371
  ```ruby
@@ -319,8 +428,17 @@ SmartCore::Types::Value::Time.refine_caster do |value, original_caster|
319
428
  # new type caster
320
429
  end
321
430
 
322
- # .refine_invariant
323
- # .refine_invariant_chain
431
+ SmartCore::Types::Value::Time.refine_runtime_attributes_checker do |value, original_checker|
432
+ # new runtime attribute checker
433
+ end
434
+
435
+ SmartCore::Types::Value::Time.refine_invariant(:name) do |value|
436
+ # new invariant
437
+ end
438
+
439
+ SmartCore::Types::Value::Time.refine_invariant_chain(:chain_name) do
440
+ # new invariant chain
441
+ end
324
442
  ```
325
443
 
326
444
  - options for type casters:
@@ -348,18 +466,16 @@ SmartCore::Types::Value::Enumerator
348
466
  SmartCore::Types::Value::EnumeratorChain
349
467
  SmartCore::Types::Value::Range
350
468
  SmartCore::Types::Value::Rational
351
- SmartCore::Types::Value::Set
352
469
  SmartCore::Types::Value::SortedSet
353
470
  SmartCore::Types::Value::IO
354
471
  SmartCore::Types::Value::StringIO
472
+ SmartCore::Types::Value::BasicObject
355
473
  SmartCore::Types::Struct::Schema
356
474
  SmartCore::Types::Struct::JSONSchema
357
475
  SmartCore::Types::Struct::StrictArray
358
476
  SmartCore::Types::Struct::StrictHash
359
477
  SmartCore::Types::Struct::Map
360
- SmartCore::Types::Variative::Enum
361
- SmartCore::Types::Variative::Variant
362
- SmartCore::Types::Protocol::InstanceOf
478
+ SmartCore::Types::Variadic::Enum
363
479
  SmartCore::Types::Protocol::Interface
364
480
  SmartCore::Types::Protocol::Ancestors
365
481
  SmartCore::Types::Protocol::Enumerable
@@ -368,6 +484,17 @@ SmartCore::Types::Protocol::Forwardable
368
484
  SmartCore::Types::Protocol::Callable
369
485
  ```
370
486
 
487
+ - `#sum` alias for `|` and `#mult` alias for `&` (with a support for type name definition and other API);
488
+
489
+ - type category in invariant error codes:
490
+ ```ruby
491
+ # before:
492
+ 'String.password.should_contain_numbers' # `String` type from `Value` category
493
+
494
+ # after:
495
+ 'Value.String.password.should_contain_numbers' # `Value::String`
496
+ ```
497
+
371
498
  - support for type of empty non-defined type (`SmartCore::Types::Primitive::Undefined`);
372
499
  - constrained types;
373
500
  - moudle-based type system integration;
@@ -391,6 +518,24 @@ SmartCore::Types::Protocol::Callable
391
518
  - Push to the branch (`git push origin feature/my-new-feature`)
392
519
  - Create new Pull Request
393
520
 
521
+ ## Build
522
+
523
+ - run tests:
524
+
525
+ ```shell
526
+ bundle exec rake rspec
527
+ # --- or ---
528
+ bundle exec rspec
529
+ ```
530
+
531
+ - run code stye linting:
532
+
533
+ ```shell
534
+ bundle exec rake rubocop
535
+ # --- or ---
536
+ bundle exec rubocop
537
+ ```
538
+
394
539
  ## License
395
540
 
396
541
  Released under MIT License.