{"id":27962616,"url":"https://github.com/rubiconmd/injectable","last_synced_at":"2025-05-07T19:56:42.944Z","repository":{"id":34911913,"uuid":"181033414","full_name":"rubiconmd/injectable","owner":"rubiconmd","description":" Opinionated and declarative Dependency Injection library for ruby.","archived":false,"fork":false,"pushed_at":"2023-12-07T02:56:42.000Z","size":92,"stargazers_count":34,"open_issues_count":2,"forks_count":5,"subscribers_count":22,"default_branch":"master","last_synced_at":"2025-05-07T19:56:36.924Z","etag":null,"topics":["dependency-injection","gem","ruby"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/injectable","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rubiconmd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-12T15:17:32.000Z","updated_at":"2024-09-30T22:48:19.000Z","dependencies_parsed_at":"2022-08-08T02:16:11.428Z","dependency_job_id":null,"html_url":"https://github.com/rubiconmd/injectable","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubiconmd%2Finjectable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubiconmd%2Finjectable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubiconmd%2Finjectable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubiconmd%2Finjectable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rubiconmd","download_url":"https://codeload.github.com/rubiconmd/injectable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252949317,"owners_count":21830150,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dependency-injection","gem","ruby"],"created_at":"2025-05-07T19:56:42.254Z","updated_at":"2025-05-07T19:56:42.933Z","avatar_url":"https://github.com/rubiconmd.png","language":"Ruby","readme":"# Injectable\n\n[![Maintainability](https://api.codeclimate.com/v1/badges/a45cc5935a5c16b837ed/maintainability)](https://codeclimate.com/github/rubiconmd/injectable/maintainability)![Ruby](https://github.com/rubiconmd/injectable/workflows/Ruby/badge.svg)\n\n`Injectable` is an opinionated and declarative [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) library for ruby.\n\nIt is being used in production (under ruby 3.1) in [RubiconMD](https://github.com/rubiconmd) and was extracted from its codebase.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'injectable', '\u003e= 1.0.0'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install injectable\n\n## Motivation\n\nThe main motivation of `Injectable` is to ease compliance with [SOLID's](https://en.wikipedia.org/wiki/SOLID)\\*, [SRP](https://en.wikipedia.org/wiki/Single_responsibility_principle)\\* and [Dependency Inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) by providing a declarative and very readable [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)\\* which avoids lots of bolierplate code and thus encourages good practices.*\n\n*Sorry about the acronyms, but using an [Ubiquitous Language](https://martinfowler.com/bliki/UbiquitousLanguage.html) is important.\n\n### Encapsulate domain logic\n\nUsing Ruby on Rails recommended practices as an example, when your application grows enough you usually end up with huge model classes with too many responsibilities.\n\nIt's way better (although it requires effort and discipline) to split those models and extract domain logic into [Service Objects](https://martinfowler.com/bliki/AnemicDomainModel.html) (\"SOs\" from now on). You can do this without `Injectable`, but `Injectable` will make your SOs way more readable and a pleasure not only to write but also to test, while encouraging general good practices.\n\n### Avoiding to hardcode dependencies\n\nIf you find occurences of `SomeClass.any_instance.expects(:method)` in your **unit** tests, then you are probably hardcoding dependencies:\n\n```rb\ntest \"MyClass#call\"\n  Collaborator.any_instance.expects(:submit!) # hardcoded dependency\n  MyClass.new.call\nend\n\nclass MyClass\n  attr_reader :collaborator\n\n  def initialize\n    @collaborator = Collaborator.new\n  end\n\n  def call\n    collaborator.submit!\n  end\nend\n```\n\nWhat if you did this instead:\n\n```rb\ntest \"MyClass#call\"\n  collaborator = stub('Collaborator')\n  collaborator.expects(:submit!)\n  MyClass.new(collaborator: collaborator).call\nend\n\nclass MyClass\n  attr_reader :collaborator\n\n  def initialize(collaborator: Collaborator.new) # we will just provide a default\n    @collaborator = collaborator\n  end\n\n  def call\n    collaborator.submit!\n  end\nend\n```\n\nThe benefits are not only for testing, as now your class is more modular and you can swap collaborators as long as they have the proper interface, in this case they have to `respond_to :submit!`\n\n`Injectable` allows you to write the above code like this:\n\n```rb\nclass MyClass\n  include Injectable\n\n  dependency :collaborator\n\n  def call\n    collaborator.submit!\n  end\nend\n```\n\nIt might not seem a lot but:\n\n1. Imagine that you have 4 dependencies. That's a lot of boilerplate.\n2. `Injectable` is not only this, it has many more features. Please keep reading.\n\n## Usage example\n\n`Injectable` is a mixin that you have to include in your class and it will provide several macros.\n\nThis is a real world example:\n\n```rb\nclass PdfGenerator\n  include Injectable\n\n  dependency :wicked_pdf\n\n  argument :html\n  argument :render_footer, default: false\n\n  def call\n    wicked_pdf.pdf_from_string(html, options)\n  end\n\n  private\n\n  def options\n    return {} unless render_footer\n\n    {\n      footer: {\n        left: footer,\n      }\n    }\n  end\n\n  def footer\n    \"Copyright ® #{Time.current.year}\"\n  end\nend\n\n# And you would use it like this:\nPdfGenerator.call(html: '\u003csome html here\u003e')\n# Overriding the wicked_pdf dependency:\nPdfGenerator.new(wicked_pdf: wicked_pdf_replacement).call(html: '\u003csome html\u003e')\n```\n\n## Premises\n\nIn order to understand how (and why) `Injectable` works, you need to know some principles.\n\n### #1 The `#call` method\n\n`Injectable` classes **must define a public `#call` method that takes no arguments**.\n\nThis is **the only public method** you will be defining in your `Injectable` classes.\n\n```rb\n# Correct ✅\ndef call\n  # do stuff\nend\n\n# Wrong ❗️\ndef call(some_argument)\n  # won't work and will raise an exception at runtime\nend\n```\n\nIf you want your `#call` method to receive arguments, that's what the `#argument` macro is for. BTW, we call those **runtime arguments**.\n\nWhy `#call`?\n\nBecause it's a ruby idiom. Many things in ruby are `callable`, like lambdas.\n\n### #2 The `initialize` method\n\nInjectable classes take their **dependencies as keyword arguments** on the `initialize` method. They can also take **configuration arguments** on `initialize`:\n\n```rb\nMyClass.new(some_dep: some_dep_instance, some_config: true).call\n```\n\n`Injectable` instantiates **dependencies that you have declared with the `dependency` macro** for you and passes them to `initialize`, so if you don't want to override those you don't even need to instantiate the class and you can use the provided class method **`#call` shortcut**:\n\n```rb\nMyclass.call # This is calling `initialize` under the hood\n```\n\nIf you need to override dependencies or configuration options, just call `new` yourself:\n\n```rb\nMyclass.new(some_dep: Override.new, some_config: false).call\n```\n\nIf you do that, **any dependency that you didn't pass will be injected by `Injectable`**.\nNotice that **configuration arguments**, which are declared with `#initialize_with` behave in the exact same way.\n\n### #3 Keyword arguments\n\nBoth `#initialize` and `#call` take **keyword arguments**.\n\n### #4 Readers\n\nAll `Injectable` macros define reader methods for you, that's why you define `#call` without arguments, because **you access everything you declare via reader methods**.\n\n## The `#dependency` macro\n\nThis is the main reason why you want to use this library in the first place.\n\nThere are several ways of declaring a `#dependency`:\n\n### Bare dependency name\n\n```rb\nclass ReportPdfRenderer\n  include Injectable\n\n  dependency :some_dependency\nend\n```\n\n1. `Injectable` first tries to find the `SomeDependency` constant in `ReportPdfRenderer`namespace.\n2. If it doesn't find it, then tries without namespace (`::SomeDependency`).\n\nNotice that this happens **at runtime**, not when defining your class.\n\n### Explicit, inline class:\n\n```rb\nclass MyInjectable\n  include Injectable\n\n  dependency :client, class: Aws::S3::Client\n  dependency :parser, class: VeryLongClassNameForMyParser\nend\n```\n\nNothing fancy here, you are explicitly telling `Injectable` which class to instantiate for you.\n\nYou will want to use this style for example if the class is namespaced somewhere else or if you want a different name other than the class', like for example if it's too long.\n\nNotice that this approach sets the class when ruby interprets the class, **not at runtime**.\n\n### With a block:\n\n```rb\ndependency :complex_client do\n  instance = ThirdPartyLib.new(:foo, bar: 'goo')\n  instance.set_config(:name, 'value')\n  instance\nend\n```\n\nIt's important to understand that `Injectable` won't call `#new` on whatever you return from this block.\n\nYou probably want to use this when your dependency has a complex setup. We use it a lot when wrapping third party libraries which aren't reused elsewhere.\n\nIf you want to wrap a third party library and you need to reuse it, then we recommend that you write a specific `Injectable` class for it, so it adheres to its principles and is easier to use.\n\n### `#dependency` options\n\n#### `:with`\n\nIf the dependency takes arguments, you can set them with :with\n\n```rb\n# Arrays will be splatted: WithNormalArguments.new(1, 2, 3)\ndependency :with_normal_arguments, with: [1, 2, 3]\n# Hashes will be passed as-is: WithKeywordArguments.new(foo: 'bar)\ndependency :with_keyword_arguments, with: { foo: 'bar' }\n```\n\n### `:depends_on`\n\nIt allows you to share **memoized instances** of dependencies and supports both a single dependency or multiples as an Array:\n\n```rb\ndependency :client # this will be instantiated just once and will be shared\ndependency :reporter, depends_on: :client\ndependency :mailer,   depends_on: %i[client reporter]\n```\n\nDependencies of dependencies will be passed as keyword arguments using the same name they were declared with. In the example above, `Injectable` will instantiate a `Mailer` class passing `{ client: client, reporter: reporter }` to `#initialize`.\n\nIf you have a dependency that is defined with a block which also depends_on other dependencies, you'll receive those as keyword arguments:\n\n```rb\ndependency :my_dependency, depends_on: :client do |client:|\n  MyDependency.new(client)\nend\n```\n\n### `:call`\n\nSometimes you have a class that doesn't adhere to `Injectable` principles:\n\n```rb\ndependency :renderer\n\ndef call\n  renderer.render # this class does not respond to `call`\nend\n```\n\n`:call` is a way of wrapping such dependency so it behaves like an `Injectable`:\n\n```rb\ndependency :renderer, call: :render\n\ndef call\n  renderer.call\nend\n```\n\nIt's important to understand that **you can mix and match all dependency configurations and options** described above.\n\n## `#initialize_with` macro\n\nThis macro is meant for **configuration arguments** passed to `initialize`:\n\n```rb\ninitialize_with :debug, default: false\n```\n\nIf you don't pass the `:default` option the argument will be required.\n\n## `#argument` macro\n\n`#argument` allows you to define **runtime arguments** passed to `#call`\n\n```rb\nargument :browser, default: 'Unknown'\n```\n\nIf you don't pass the `:default` option the argument will be required.\n\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\nPlease consider configuring [https://editorconfig.org/] on your favourite IDE/editor, so basic file formatting is consistent and avoids cross-platform issues. Some editors require [a plugin](https://editorconfig.org/#download), meanwhile others have it [pre-installed](https://editorconfig.org/#pre-installed).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/rubiconmd/injectable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the Injectable project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).\n\n## Credits\n\n- [RubiconMD](https://github.com/rubiconmd) allowed extracting this gem from its codebase and release it as open source.\n- [Durran Jordan](https://github.com/durran) allowed the usage of the gem name at rubygems.org.\n- [David Marchante](https://github.com/iovis) brainstormed the `initialize`/`call` approach, did all code reviews and provided lots of insightful feedback and suggestions. He also wrote the inline documentation.\n- [Julio Antequera](https://github.com/jantequera), [Jimmi Carney](https://github.com/ayoformayo) and [Anthony Rocco](https://github.com/amrocco) had the patience to use it and report many bugs. Also most of the features in this gem came up when reviewing their usage of it. Anthony also made the effort of extracting the code from RubiconMD's codebase.\n- [Rodrigo Álvarez](https://github.com/Papipo) had the idea for the DSL and actually wrote the library.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubiconmd%2Finjectable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frubiconmd%2Finjectable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubiconmd%2Finjectable/lists"}