├── .github ├── README.md └── LICENSE ├── di.cppm └── di /.github/README.md: -------------------------------------------------------------------------------- 1 | ../di -------------------------------------------------------------------------------- /.github/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Kris Jusiak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /di.cppm: -------------------------------------------------------------------------------- 1 | // 26 | [Overview](#Overview) / [Examples](#Examples) / [API](#API) / [FAQ](#FAQ) / [Resources](#Resources) 27 | 28 | ## `DI`: Dependency Injection library 29 | 30 | [![MIT Licence](http://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/license/mit) 31 | [![Version](https://img.shields.io/github/v/release/qlibs/di)](https://github.com/qlibs/di/releases) 32 | [![Build](https://img.shields.io/badge/build-green.svg)](https://godbolt.org/z/fKEcojqze) 33 | [![Try it online](https://img.shields.io/badge/try%20it-online-blue.svg)](https://godbolt.org/z/xrzsYG1bj) 34 | 35 | > https://en.wikipedia.org/wiki/Dependency_injection 36 | 37 | ### Features 38 | 39 | - Single header (https://raw.githubusercontent.com/qlibs/di/main/di) / C++20 module (https://raw.githubusercontent.com/qlibs/di/main/di.cppm) 40 | - Verifies itself upon include (can be disabled with `-DNTEST` - see [FAQ](#faq)) 41 | - Minimal [API](#api) 42 | - Unified way for different polymorphism styles (`inheritance, type erasure, variant, ...`) 43 | - [Generic factories](https://en.wikipedia.org/wiki/Factory_method_pattern) 44 | - Constructor deduction for classes and aggregates 45 | - Constructor order and types changes agnostic (simplifies integration with `third party` libraries) 46 | - Testing (different bindigns for `production` and `testing`, `faking` some parameters with `assisted` injection) 47 | - Policies (APIs with `checked` requirements) 48 | - Logging/Profiling/Serialization/... (via iteration over all `created` objects) 49 | 50 | ### Requirements 51 | 52 | - C++20 ([clang++13+, g++11+](https://en.cppreference.com/w/cpp/compiler_support)) 53 | 54 | ### Overview 55 | 56 | > API (https://godbolt.org/z/xrzsYG1bj) 57 | 58 | ```cpp 59 | struct aggregate1 { int i1{}; int i2{}; }; 60 | struct aggregate2 { int i2{}; int i1{}; }; 61 | struct aggregate { aggregate1 a1{}; aggregate2 a2{}; }; 62 | 63 | // di::make (basic) 64 | { 65 | static_assert(42 == di::make(42)); 66 | static_assert(aggregate1{1, 2} == di::make(1, 2)); 67 | } 68 | 69 | // di::make (generic) 70 | { 71 | auto a = di::make(di::overload{ 72 | [](di::trait auto) { return 42; } 73 | }); 74 | 75 | assert(a.i1 == 42); 76 | assert(a.i2 == 42); 77 | } 78 | 79 | // di::make (assisted) 80 | { 81 | struct assisted { 82 | constexpr assisted(int i, aggregate a, float f) : i{i}, a{a}, f{f} { } 83 | int i{}; 84 | aggregate a{}; 85 | float f{}; 86 | }; 87 | 88 | auto fakeit = [](auto t) { return decltype(t.type()){}; }; 89 | auto a = di::make(999, di::make(fakeit), 4.2f); 90 | 91 | assert(a.i == 999); 92 | assert(a.a.a1.i1 == 0); 93 | assert(a.a.a1.i2 == 0); 94 | assert(a.a.a2.i1 == 0); 95 | assert(a.a.a2.i2 == 0); 96 | assert(a.f == 4.2f); 97 | } 98 | 99 | // di::make (with names) 100 | { 101 | auto a = di::make(di::overload{ 102 | [](di::is auto t) requires (t.name() == "i1") { return 4; }, 103 | [](di::is auto t) requires (t.name() == "i2") { return 2; }, 104 | }); 105 | 106 | assert(a.i1 == 4); 107 | assert(a.i2 == 2); 108 | } 109 | 110 | // di::make (with names) - reverse order 111 | { 112 | auto a = di::make(di::overload{ 113 | [](di::is auto t) requires (t.name() == "i1") { return 4; }, 114 | [](di::is auto t) requires (t.name() == "i2") { return 2; }, 115 | }); 116 | 117 | assert(a.i1 == 4); 118 | assert(a.i2 == 2); 119 | } 120 | 121 | // di::make (with names, context and compound types) 122 | { 123 | auto a = di::make(di::overload{ 124 | // custom bindigs 125 | [](di::trait auto t) 126 | requires (t.name() == "i1" and 127 | &typeid(t.parent().type()) == &typeid(aggregate1)) { return 99; }, 128 | [](di::trait auto) { return 42; }, 129 | 130 | // generic bindings 131 | [](auto t) -> decltype(auto) { return di::make(t); }, // compund types 132 | }); 133 | 134 | assert(a.a1.i1 == 99); 135 | assert(a.a1.i2 == 42); 136 | assert(a.a2.i1 == 42); 137 | assert(a.a2.i2 == 42); 138 | } 139 | 140 | constexpr auto generic = di::overload{ 141 | [](auto t) -> decltype(auto) { return di::make(t); }, // compund types 142 | }; 143 | 144 | // di::make (seperate overloads) 145 | { 146 | constexpr auto custom = di::overload { 147 | [](di::trait auto t) 148 | requires (t.name() == "i1" and 149 | &typeid(t.parent().type()) == &typeid(aggregate1)) { return 99; }, 150 | [](di::trait auto t) { return decltype(t.type()){}; }, 151 | }; 152 | 153 | auto a = di::make(di::overload{custom, generic}); 154 | 155 | assert(a.a1.i1 == 99); 156 | assert(a.a1.i2 == 0); 157 | assert(a.a2.i1 == 0); 158 | assert(a.a2.i2 == 0); 159 | } 160 | 161 | // di::make (polymorphism, scopes) 162 | { 163 | struct interface { 164 | constexpr virtual ~interface() noexcept = default; 165 | constexpr virtual auto fn() const -> int = 0; 166 | }; 167 | struct implementation : interface { 168 | constexpr implementation(int i) : i{i} { } 169 | constexpr auto fn() const -> int override final { return i; } 170 | int i{}; 171 | }; 172 | 173 | struct example { 174 | example( 175 | aggregate& a, 176 | const std::shared_ptr& sp 177 | ) : a{a}, sp{sp} { } 178 | aggregate a{}; 179 | std::shared_ptr sp{}; 180 | }; 181 | 182 | auto i = 123; 183 | 184 | auto bindings = di::overload{ 185 | generic, 186 | 187 | [](di::is auto t) { return di::make(t); }, 188 | [&](di::is auto) -> decltype(auto) { return i; }, // instance 189 | 190 | // scopes 191 | [](di::trait auto t) -> decltype(auto) { 192 | using type = decltype(t.type()); 193 | static auto singleton{di::make>(t)}; 194 | return (singleton); 195 | }, 196 | }; 197 | 198 | auto e = di::make(bindings); 199 | 200 | assert(123 == e.sp->fn()); 201 | assert(123 == e.a.a1.i1); 202 | assert(123 == e.a.a1.i2); 203 | assert(123 == e.a.a2.i1); 204 | assert(123 == e.a.a2.i2); 205 | 206 | // testing (override bindings) 207 | { 208 | auto testing = di::overload{ 209 | [](di::trait auto) { return 1000; }, // priority 210 | [bindings](auto t) -> decltype(auto) { return bindings(t); }, // otherwise 211 | }; 212 | 213 | auto e = di::make(testing); 214 | 215 | assert(1000 == e.sp->fn()); 216 | assert(1000 == e.a.a1.i1); 217 | assert(1000 == e.a.a1.i2); 218 | assert(1000 == e.a.a2.i1); 219 | assert(1000 == e.a.a2.i2); 220 | } 221 | 222 | // logging 223 | { 224 | constexpr auto logger = [root = false]( 225 | di::provider&& t) mutable -> decltype(auto) { 226 | if constexpr (constexpr auto is_root = 227 | di::provider::size() == 1u; is_root) { 228 | if (not std::exchange(root, true)) { 229 | std::clog << reflect::type_name() << '\n'; 230 | } 231 | } 232 | for (auto i = 0u; i < di::provider::size(); ++i) { 233 | std::clog << ' '; 234 | } 235 | if constexpr (di::is_smart_ptr>) { 236 | std::clog << reflect::type_name() << '<' 237 | << reflect::type_name< 238 | typename std::remove_cvref_t::element_type>() << '>'; 239 | } else { 240 | std::clog << reflect::type_name(); 241 | } 242 | if constexpr (not di::is_smart_ptr> and 243 | requires { std::clog << std::declval(); }) { 244 | std::clog << ':' << t(t); 245 | } 246 | std::clog << '\n'; 247 | 248 | return t(t); 249 | }; 250 | 251 | (void)di::make(di::overload{logger, bindings}); 252 | // example 253 | // aggregate 254 | // aggregate1 255 | // int:123 256 | // int:123 257 | // aggregate2 258 | // int:123 259 | // int:123 260 | // shared_ptr -> implmentation 261 | // int:123 262 | } 263 | } 264 | 265 | // policies 266 | { 267 | struct policy { 268 | constexpr policy(int*) { } 269 | }; 270 | 271 | [[maybe_unused]] auto p = di::make(di::overload{ 272 | []([[maybe_unused]] di::trait auto t) { 273 | static_assert(not sizeof(t), "raw pointers are not allowed!"); 274 | }, 275 | [](auto t) -> decltype(auto) { return di::make(t); }, // compund types 276 | }); // error 277 | } 278 | 279 | // errors 280 | { 281 | (void)di::make(di::overload{ 282 | // [](di::is auto) { return 42; }, // missing binding 283 | [](auto t) { return di::make(t); }, 284 | }); // di::error 285 | } 286 | 287 | // and more (see API)... 288 | ``` 289 | 290 | ### Examples 291 | 292 | > DIY - Dependency Injection Yourself (https://godbolt.org/z/acE3rYar5) 293 | 294 | ```cpp 295 | namespace di { 296 | inline constexpr auto injector = [](auto&&... ts) { 297 | return di::overload{ 298 | std::forward(ts)..., 299 | [](di::trait auto t) -> decltype(auto) { 300 | using type = decltype(t.type()); 301 | static auto singleton{di::make>(t)}; 302 | return (singleton); 303 | }, 304 | [](auto t) { return di::make(t); }, 305 | }; 306 | }; 307 | template 308 | inline constexpr auto bind = [] { 309 | if constexpr (std::is_void_v) { 310 | return [](T&& to) { 311 | return [&](di::is auto) -> decltype(auto) { 312 | return std::forward(to); 313 | }; 314 | }; 315 | } else { 316 | return [](di::is auto t) { return di::make(t); }; 317 | } 318 | }(); 319 | } // namespace di 320 | ``` 321 | 322 | ```cpp 323 | int main() { 324 | auto injector = di::injector( 325 | di::bind, 326 | di::bind(42) 327 | ); 328 | 329 | auto e = di::make(injector); 330 | 331 | assert(42 == e.sp->fn()); 332 | assert(42 == e.a.a1.i1); 333 | assert(42 == e.a.a1.i2); 334 | assert(42 == e.a.a2.i1); 335 | assert(42 == e.a.a2.i2); 336 | } 337 | ``` 338 | 339 | > Standard Template Library (https://godbolt.org/z/jjbnffKne) 340 | 341 | ```cpp 342 | struct STL { 343 | STL(std::vector vector, 344 | std::shared_ptr shared_ptr, 345 | std::unique_ptr unique_ptr, 346 | std::array array, 347 | std::string string) 348 | : vector(vector) 349 | , shared_ptr(shared_ptr) 350 | , unique_ptr(std::move(unique_ptr)) 351 | , array(array) 352 | , string(string) 353 | { } 354 | 355 | std::vector vector; 356 | std::shared_ptr shared_ptr; 357 | std::unique_ptr unique_ptr; 358 | std::array array; 359 | std::string string; 360 | }; 361 | 362 | int main() { 363 | auto stl = di::make( 364 | di::overload{ 365 | [](di::is> auto) { return std::vector{1, 2, 3}; }, 366 | [](di::is> auto) { return std::make_shared(1); }, 367 | [](di::is> auto) { return std::make_unique(2); }, 368 | [](di::is> auto) { return std::array{3}; }, 369 | [](di::is auto) { return std::string{"di"}; }, 370 | [](auto t) { return di::make(t); }, 371 | } 372 | ); 373 | 374 | assert(3u == stl.vector.size()); 375 | assert(1 == stl.vector[0]); 376 | assert(2 == stl.vector[1]); 377 | assert(3 == stl.vector[2]); 378 | assert(1 == *static_cast(stl.shared_ptr.get())); 379 | assert(2 == *stl.unique_ptr); 380 | assert(3 == stl.array[0]); 381 | assert("di" == stl.string); 382 | } 383 | ``` 384 | 385 | > `is_structural` - https://eel.is/c++draft/temp.param#def:type,structural (https://godbolt.org/z/1Mrxfbaqb) 386 | 387 | ```cpp 388 | template; 391 | if constexpr (requires { type{}; }) { 392 | return type{}; 393 | } else { 394 | return di::make(t); 395 | } 396 | } 397 | > concept is_structural = requires { [](cfg)>{}(); }; 398 | 399 | static_assert(is_structural); 400 | static_assert(not is_structural>); 401 | 402 | struct s { s() = delete; }; 403 | static_assert(not is_structural); 404 | 405 | struct y { int i; }; 406 | static_assert(is_structural); 407 | 408 | struct n { private: int i; }; 409 | static_assert(not is_structural); 410 | 411 | struct c1 { constexpr c1(int) {} }; 412 | static_assert(is_structural); 413 | 414 | struct c2 { constexpr c2(int, double) {} }; 415 | static_assert(is_structural); 416 | 417 | struct c3 { constexpr c3(std::optional) {} }; 418 | static_assert(not is_structural); 419 | 420 | struct c4 { constexpr c4(auto...) {} }; 421 | static_assert(is_structural); 422 | 423 | struct c5 { private: constexpr c5(auto...) {} }; 424 | static_assert(not is_structural); 425 | ``` 426 | 427 | ### API 428 | 429 | ```cpp 430 | namespace di::inline v1_0_6 { 431 | /** 432 | * @code 433 | * struct c1 { c1(int) { } }; 434 | * static_assert(std::is_same_v, di::ctor_traits::type>); 435 | * #endcode 436 | */ 437 | template struct ctor_traits { 438 | template struct type_list{}; 439 | using type = type_list; 440 | [[nodiscard]] constexpr auto operator()(auto&&...) const -> T; 441 | }; 442 | 443 | /** 444 | * static_assert(di::invocable); 445 | * static_assert(di::invocable); 446 | */ 447 | template concept invocable; 448 | 449 | /** 450 | * @code 451 | * static_assert(not di::is); 452 | * static_assert(di::is); 453 | * @endcode 454 | */ 455 | template concept is; 456 | 457 | /** 458 | * @code 459 | * static_assert(not di::is_a); 460 | * static_assert(di::is_a, std::shared_ptr>); 461 | */ 462 | template class R> concept is_a; 463 | 464 | /** 465 | * @code 466 | * static_assert(not di::is_smart_ptr); 467 | * static_assert(di::is_smart_ptr>); 468 | */ 469 | template concept is_smart_ptr; 470 | 471 | /** 472 | * @code 473 | * static_assert(not di::trait); 474 | * static_assert(di::trait); 475 | */ 476 | template class Trait> concept trait; 477 | 478 | /** 479 | * @code 480 | * static_assert(42 == di::overload{ 481 | * [](int i) { return i; }, 482 | * [](auto a) { return a; } 483 | * }(42)); 484 | * @endcode 485 | */ 486 | template struct overload; 487 | 488 | /** 489 | * Injection context 490 | */ 491 | template 492 | struct provider { 493 | using value_type = T; 494 | using parent_type = TParent; 495 | 496 | static constexpr auto index() -> std::size_t; // index of parent constructor 497 | static constexpr auto parent() -> parent_type; // callee provider 498 | static constexpr auto type() -> value_type; // underlying type 499 | static constexpr auto size() -> std::size_t; // size of parents 500 | #if defined(REFLECT) 501 | static constexpr auto name() -> std::string_view; // member name 502 | #endif 503 | }; 504 | 505 | /** 506 | * @code 507 | * static_assert(42 == di::make(42)); 508 | * static_assert(42 == di::make( 509 | * di::overload{ 510 | * [](di::is auto) { return 42; } 511 | * } 512 | * )); 513 | * @endcode 514 | */ 515 | template 516 | [[nodiscard]] constexpr auto make(auto&&...); 517 | } // namespace di 518 | ``` 519 | 520 | ### FAQ 521 | 522 | > - Dependency Injection? 523 | > 524 | > Dependency Injection (DI) - https://en.wikipedia.org/wiki/Dependency_injection - it's a technique focusing on producing loosely coupled code. 525 | > 526 | > ```cpp 527 | > struct no_di { 528 | > constexpr no_di() { } // No DI 529 | > 530 | > private: 531 | > int data = 42; // coupled 532 | > }; 533 | > 534 | > struct di { 535 | > constexpr di(int data) : data{data} { } // DI 536 | > 537 | > private: 538 | > int data{}; 539 | > }; 540 | > ``` 541 | > 542 | > - In a very simplistic view, DI is about passing objects/types/etc via constructors and/or other forms of parameter propagating techniques instead of coupling values/types directly (`Hollywood Principle - Don't call us we'll call you`). 543 | > - The main goal of DI is the flexibility of changing what's being injected. It's important though, what and how is being injected as that influences how good (`ETC - Easy To Change`) the design will be - more about it here - https://www.youtube.com/watch?v=yVogS4NbL6U. 544 | > 545 | > - Manual vs Automatic Dependency Injection? 546 | > 547 | > Depedency Injection doesnt imply using a library. 548 | > Automatic DI requires a library and makes more sense for larger projects as it helps limitting the wiring mess and the maintenance burden assosiated with it. 549 | > 550 | > ```cpp 551 | > struct coffee_maker { 552 | > coffee_maker(); // No DI 553 | > 554 | > private: 555 | > basic_heater heater{}; // coupled 556 | > basic_pump pump{}; // coupled 557 | > }; 558 | > 559 | > struct coffee_maker_v1 { 560 | > coffee_maker(iheater&, ipump& pump); // DI 561 | > 562 | > private: 563 | > iheater& heater; // not coupled 564 | > ipump& pump; // not coupled 565 | > }; 566 | > 567 | > struct coffee_maker_v2 { 568 | > coffee_maker(std::shared_ptr, std::unique_ptr); // DI 569 | > 570 | > private: 571 | > std::shared_ptr pump; // not coupled 572 | > std::unique_ptr heater; // not coupled 573 | > }; 574 | > 575 | > int main() { 576 | > // Manual Dependency Injection 577 | > { 578 | > basic_heater heater{}; 579 | > basic_pump pump{}; 580 | > coffe_maker_v1 cm{heater, pump}; 581 | > } 582 | > { 583 | > auto pump = std::make_shared(); 584 | > auto heater = std::make_unique(); 585 | > coffe_maker_v2 cm{pump, std::move(heater)}; // different wiring 586 | > } 587 | > 588 | > // Automatic Dependency Injection 589 | > auto wiring = di::overload{ 590 | > [](di::is auto) { return make(); }, 591 | > [](di::is auto) { return make(); }, 592 | > }; 593 | > { 594 | > auto cm = di::make(wiring); 595 | > } 596 | > { 597 | > auto cm = di::make(wiring); // same wiring 598 | > } 599 | > } 600 | > ``` 601 | > 602 | > The main goal of automatic is to **avoid design compromises** in order to reduce the boilerplate code/minimize maintance burden/simplify testing. 603 | > 604 | > - How does it work? 605 | > 606 | > `DI` works by deducing constructor parameters and calling appropriate overload to handle them by leavaring concepts - https://eel.is/c++draft/temp.constr.order#def:constraint,subsumption. 607 | > The following represents the most important parts of the library design. 608 | > 609 | > ```cpp 610 | > template 611 | > concept copy_or_move = std::is_same_v>; 612 | > 613 | > template struct any { 614 | > template requires (not copy_or_move) 615 | > operator T() noexcept(noexcept(bind, T>{})); 616 | > template requires (not copy_or_move) 617 | > operator T&() const noexcept(noexcept(bind, T&>{})); 618 | > template requires (not copy_or_move) 619 | > operator const T&() const noexcept(noexcept(bind, const T&>{})); 620 | > template requires (not copy_or_move) 621 | > operator T&&() const noexcept(noexcept(bind, T&&>{})); 622 | > }; 623 | > ``` 624 | > 625 | > ```cpp 626 | > template constexpr auto ctor_traits() { 627 | > return [](std::index_sequence) { 628 | > if constexpr (requires { T{any{}...}; }) { 629 | > return type_list{}))...>{}; 630 | > } else if constexpr (sizeof...(Ns)) { 631 | > return ctor_traits(); 632 | > } else { 633 | > return type_list{}; 634 | > } 635 | > }(std::make_index_sequence{}); 636 | > } 637 | > ``` 638 | > 639 | > ```cpp 640 | > template struct overload : Ts... { using Ts::operator()...; }; 641 | > template overload(Ts...) -> overload; 642 | > ``` 643 | > 644 | > ```cpp 645 | > template auto error(auto&&...) -> T; 646 | > template constexpr auto make(invocable auto&& t) { 647 | > return [&] class TList, class... Ts>(TList) { 648 | > if constexpr (requires { T{t(provider(t)...); }; }) { 649 | > return T{t(provider(t)...}; 650 | > } else { 651 | > return error(t); 652 | > } 653 | > }(ctor_traits()); 654 | > }; 655 | > ``` 656 | > 657 | > - How to disable running tests at compile-time? 658 | > 659 | > When `-DNTEST` is defined static_asserts tests wont be executed upon include. 660 | > Note: Use with caution as disabling tests means that there are no gurantees upon include that given compiler/env combination works as expected. 661 | > 662 | > - Similar projects? 663 | > [boost-ext.di](https://github.com/boost-ext/di), [google.fruit](https://github.com/google/fruit), [kangaru](https://github.com/gracicot/kangaru), [wallaroo](https://wallaroolib.sourceforge.net), [hypodermic](https://github.com/ybainier/Hypodermic), [dingo](https://github.com/romanpauk/dingo) 664 | 665 | ### Resources 666 | 667 | > - Dependency Injection - a 25-dollar term for a 5-cent concept - https://www.youtube.com/watch?v=yVogS4NbL6U 668 | > - Law of Demeter: A Practical Guide to Loose Coupling - https://www.youtube.com/watch?v=QZkVpZlbM4U 669 | > - Clean Code: A Handbook of Agile Software Craftsmanship - https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882 670 | > - The Pragmatic Programmer - https://www.amazon.com/Pragmatic-Programmer-journey-mastery-Anniversary/dp/0135957052 671 | > - Design Patterns - https://www.amazon.com/Design-Patterns-Object-Oriented-Addison-Wesley-Professional-ebook/dp/B000SEIBB8 672 | > - Test Driven Development: By Example - https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530 673 | 674 | ### License 675 | 676 | > - [MIT](LICENSE) 677 | 678 |