Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServiceContainer Refactor #8

Closed
wants to merge 10 commits into from
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2020 Blacksmoke16
Copyright (c) 2020 George Dietrich

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ Everything is documented in the [API Docs](https://athena-framework.github.io/de

## Contributors

- [Blacksmoke16](https://github.com/blacksmoke16) - creator and maintainer
- [George Dietrich](https://github.com/blacksmoke16) - creator and maintainer
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: |
Flexible instance based dependency injection service container library.

authors:
- Blacksmoke16 <[email protected]>
- George Dietrich <[email protected]>

development_dependencies:
ameba:
Expand Down
2 changes: 1 addition & 1 deletion spec/compiler_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "./spec_helper"

describe Athena::DependencyInjection do
pending Athena::DependencyInjection do
describe Athena::DependencyInjection::ServiceContainer do
describe "compiler errors" do
describe "when trying to access a private service directly" do
Expand Down
2 changes: 1 addition & 1 deletion spec/injectable_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "./spec_helper"

describe ADI::Injectable do
pending ADI::Injectable do
describe "with only services" do
it "should inject an instance of the Store class" do
klass = SomeClass.new
Expand Down
2 changes: 1 addition & 1 deletion spec/service_container_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "./spec_helper"

describe ADI::ServiceContainer do
pending ADI::ServiceContainer do
describe "#get" do
describe "by type" do
it "should return an array of services with that type" do
Expand Down
184 changes: 112 additions & 72 deletions src/athena-dependency_injection.cr
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ alias ADI = Athena::DependencyInjection
# Using interfaces allows changing the functionality of a type by just changing what service gets injected into it.
# See this [blog post](https://dev.to/blacksmoke16/dependency-injection-in-crystal-2d66#plug-and-play) for an example of this.
module Athena::DependencyInjection
module CompilerPass
macro included
macro pre_process(service_hash, alias_hash)
end

macro post_process(service_hash, alias_hash)
end
end
end

# Stores metadata associated with a specific service.
#
# The type of the service affects how it behaves within the container. When a `struct` service is retrieved or injected into a type, it will be a copy of the one in the SC (passed by value).
Expand Down Expand Up @@ -152,6 +162,7 @@ module Athena::DependencyInjection
# end
# ```
annotation Register; end
annotation Inject; end

# Used to designate a type as a service.
#
Expand All @@ -162,78 +173,107 @@ module Athena::DependencyInjection
def self.container : ADI::ServiceContainer
Fiber.current.container
end
end

# Adds a new constructor that resolves the required services based on type and name.
#
# Can be included into a `class`/`struct` in order to automatically inject the required services from the container based on the type's initializer.
#
# Service lookup is based on the type restriction and name of the initializer arguments. If there is only a single service
# of the required type, then that service is used. If there are multiple services of the required type then the name of the argument's name is used.
# An exception is raised if a service was not able to be resolved.
#
# ## Examples
#
# ### Default Usage
#
# ```
# @[ADI::Register]
# class Store
# include ADI::Service
#
# property uuid : String = "UUID"
# end
#
# class MyNonService
# include ADI::Injectable
#
# getter store : Store
#
# def initialize(@store : Store); end
# end
#
# MyNonService.new.store.uuid # => "UUID"
# ```
#
# ### Non Service Dependencies
#
# Named arguments take precedence. This allows dependencies to be supplied explicitly without going through the resolving process; such as for testing.
# ```
# @[ADI::Register]
# class Store
# include ADI::Service
#
# property uuid : String = "UUID"
# end
#
# class MyNonService
# include ADI::Injectable
#
# getter store : Store
# getter id : String
#
# def initialize(@store : Store, @id : String); end
# end
#
# service = MyNonService.new(id: "FOO")
# service.store.uuid # => "UUID"
# service.id # => "FOO"
# ```
module Injectable
macro included
macro finished
{% verbatim do %}
{% if initializer = @type.methods.find &.name.stringify.==("initialize") %}
# Auto generated via `ADI::Injectable` module.
def self.new(**args)
new(
{% for arg in initializer.args %}
{{arg.name.id}}: args[{{arg.name.symbolize}}]? || ADI.container.resolve({{arg.restriction.id}}, {{arg.name.stringify}}),
{% end %}
)
end
{% end %}
{% end %}
end
end
abstract class FakeServices
end

@[ADI::Register]
class FakeService < FakeServices
include ADI::Service
end

@[ADI::Register(name: "custom_fake", alias: FakeServices)]
class CustomFooFakeService < FakeServices
include ADI::Service
end

@[ADI::Register(_name: "JIM")]
class Bar
include ADI::Service

def initialize(@asdf : FakeServices, @name : String); end
end

@[ADI::Register]
class FooBar
include ADI::Service

def initialize(@obj : Foo); end
end

@[ADI::Register(1, "fred", false)]
class Foo
include ADI::Service

def initialize(@id : Int32, @name : String, @active : Bool); end
end

@[ADI::Register]
class Blah
include ADI::Service
end

@[ADI::Register(decorates: "blah")]
class BlahDecorator
include ADI::Service

def initialize(@blah : Blah); end
end

@[ADI::Register("@?blah")]
class Baz
include ADI::Service

def initialize(@blah : Blah?); end
end

@[ADI::Register]
class Athena::RoutingStuff::Public
include ADI::Service

def initialize
# pp "new public"
end
end

@[ADI::Register(lazy: true)]
class Lazy
include ADI::Service

def initialize
# pp "new lazy"
end
end

@[ADI::Register("GOOGLE", "Google", name: "google", tags: ["feed_partner", "partner"])]
@[ADI::Register("FACEBOOK", "Facebook", name: "facebook", tags: ["partner"])]
struct FeedPartner
include ADI::Service

getter id : String
getter name : String

def initialize(@id : String, @name : String); end
end

@[ADI::Register("!partner")]
class PartnerManager
include ADI::Service

getter partners

def initialize(@partners : Array(FeedPartner))
end
end

@[ADI::Register("@fake_services")]
class Test
include ADI::Service

def initialize(@t : FakeServices); end
end

CONTAINER = ADI::ServiceContainer.new

pp CONTAINER
Loading