Skip to content

Commit 9f214b6

Browse files
authored
Centralize tag processing (athena-framework/athena#650)
## Context Little refactor to allow centralizing service tag processing. Tags are collected as raw values, then normalized into the final hash format via a dedicated compiler pass.
1 parent 9c61c23 commit 9f214b6

6 files changed

Lines changed: 69 additions & 81 deletions

File tree

spec/compiler_passes/register_services_spec.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ describe ADI::ServiceContainer::RegisterServices do
158158

159159
describe "tags" do
160160
it "errors if not all tags have a `name` field" do
161-
assert_compile_time_error "Failed to register service 'foo'. Tag must have a name.", <<-CR
161+
assert_compile_time_error "Failed to register service 'foo' (Foo). Tag must have a name.", <<-CR
162162
@[ADI::Register(tags: [{priority: 100}])]
163163
record Foo
164164
CR

src/compiler_passes/normalize_definitions.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module Athena::DependencyInjection::ServiceContainer::NormalizeDefinitions
2727
end
2828

2929
unless definition_keys.includes? "tags"
30-
definition["tags"] = {} of Nil => Nil
30+
definition["tags"] = [] of Nil
3131
end
3232

3333
unless definition_keys.includes? "bindings"

src/compiler_passes/process_autoconfigure_annotations.cr

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -104,41 +104,9 @@ module Athena::DependencyInjection::ServiceContainer::ProcessAutoconfigureAnnota
104104
end
105105
end
106106

107-
# TODO: Centralize tag handling logic between AutoConfigure and RegisterServices
107+
# Append raw tags - will be normalized by ProcessTags pass
108108
tags.each do |tag|
109-
name, attributes = if tag.is_a?(StringLiteral)
110-
{tag, {} of Nil => Nil}
111-
elsif tag.is_a?(Path)
112-
{tag.resolve.id.stringify, {} of Nil => Nil}
113-
elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral)
114-
unless tag[:name]
115-
tag.raise "Failed to register service '#{service_id.id}' (#{klass}). Tag must have a name."
116-
end
117-
118-
# Resolve a constant to its value if used as a tag name
119-
if tag["name"].is_a? Path
120-
tag["name"] = tag["name"].resolve
121-
end
122-
123-
attributes = {} of Nil => Nil
124-
125-
# TODO: Replace this with `#delete`...
126-
tag.each do |k, v|
127-
attributes[k.id.stringify] = v unless k.id.stringify == "name"
128-
end
129-
130-
{tag["name"], attributes}
131-
else
132-
tag.raise "Tag must be a 'StringLiteral' or 'NamedTupleLiteral', got '#{tag.class_name.id}'."
133-
end
134-
135-
definition["tags"][name] = [] of Nil if definition["tags"][name] == nil
136-
definition["tags"][name] << attributes
137-
definition["tags"][name] = definition["tags"][name].uniq
138-
139-
TAG_HASH[name] = [] of Nil if TAG_HASH[name] == nil
140-
TAG_HASH[name] << {service_id, attributes}
141-
TAG_HASH[name] = TAG_HASH[name].uniq
109+
definition["tags"] << tag
142110
end
143111
end
144112
end
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# :nodoc:
2+
#
3+
# Normalizes service tags and populates TAG_HASH.
4+
# Runs after RegisterServices and ProcessAutoconfigureAnnotations.
5+
module Athena::DependencyInjection::ServiceContainer::ProcessTags
6+
macro included
7+
macro finished
8+
{% verbatim do %}
9+
{%
10+
SERVICE_HASH.each do |service_id, definition|
11+
klass = definition["class"]
12+
normalized_tags = {} of Nil => Nil
13+
14+
(definition["tags"] || [] of Nil).each do |tag|
15+
name, attributes = if tag.is_a?(StringLiteral)
16+
{tag, {} of Nil => Nil}
17+
elsif tag.is_a?(Path)
18+
{tag.resolve.id.stringify, {} of Nil => Nil}
19+
elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral)
20+
unless tag[:name]
21+
tag.raise "Failed to register service '#{service_id.id}' (#{klass}). Tag must have a name."
22+
end
23+
24+
# Resolve a constant to its value if used as a tag name
25+
if tag["name"].is_a? Path
26+
tag["name"] = tag["name"].resolve
27+
end
28+
29+
# TODO: Replace this with `#delete` if/when it's ever released
30+
# https://github.com/crystal-lang/crystal/pull/9837
31+
attributes = {} of Nil => Nil
32+
33+
tag.each do |k, v|
34+
attributes[k.id.stringify] = v unless k.id.stringify == "name"
35+
end
36+
37+
{tag["name"], attributes}
38+
else
39+
tag.raise "Tag must be a 'StringLiteral' or 'NamedTupleLiteral', got '#{tag.class_name.id}'."
40+
end
41+
42+
normalized_tags[name] = [] of Nil if normalized_tags[name] == nil
43+
normalized_tags[name] << attributes
44+
normalized_tags[name] = normalized_tags[name].uniq
45+
46+
TAG_HASH[name] = [] of Nil if TAG_HASH[name] == nil
47+
TAG_HASH[name] << {service_id, attributes}
48+
TAG_HASH[name] = TAG_HASH[name].uniq
49+
end
50+
51+
definition["tags"] = normalized_tags
52+
end
53+
%}
54+
{% end %}
55+
end
56+
end
57+
end

src/compiler_passes/register_services.cr

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,51 +50,13 @@ module Athena::DependencyInjection::ServiceContainer::RegisterServices
5050
end
5151
%}
5252

53-
{%
54-
definition_tags = {} of Nil => Nil
55-
tags = ann["tags"] || [] of Nil
53+
{% # Store raw tags - will be normalized by ProcessTags pass
5654

57-
unless tags.is_a? ArrayLiteral
58-
ann["tags"].raise "'tags' field of service '#{service_id.id}' must be an 'ArrayLiteral', got '#{tags.class_name.id}'."
59-
end
55+
tags = ann["tags"] || [] of Nil
6056

61-
# TODO: Centralize tag handling logic between AutoConfigure and RegisterServices
62-
tags.each do |tag|
63-
name, attributes = if tag.is_a?(StringLiteral)
64-
{tag, {} of Nil => Nil}
65-
elsif tag.is_a?(Path)
66-
{tag.resolve.id.stringify, {} of Nil => Nil}
67-
elsif tag.is_a?(NamedTupleLiteral) || tag.is_a?(HashLiteral)
68-
unless tag[:name]
69-
tag.raise "Failed to register service '#{service_id.id}'. Tag must have a name."
70-
end
71-
72-
# Resolve a constant to its value if used as a tag name
73-
if tag["name"].is_a? Path
74-
tag["name"] = tag["name"].resolve
75-
end
76-
77-
attributes = {} of Nil => Nil
78-
79-
# TODO: Replace this with `#delete`...
80-
tag.each do |k, v|
81-
attributes[k.id.stringify] = v unless k.id.stringify == "name"
82-
end
83-
84-
{tag["name"], attributes}
85-
else
86-
tag.raise "Tag must be a 'StringLiteral' or 'NamedTupleLiteral', got '#{tag.class_name.id}'."
87-
end
88-
89-
definition_tags[name] = [] of Nil if definition_tags[name] == nil
90-
definition_tags[name] << attributes
91-
definition_tags[name] = definition_tags[name].uniq
92-
93-
TAG_HASH[name] = [] of Nil if TAG_HASH[name] == nil
94-
TAG_HASH[name] << {service_id, attributes}
95-
TAG_HASH[name] = TAG_HASH[name].uniq
96-
end
97-
%}
57+
unless tags.is_a? ArrayLiteral
58+
ann["tags"].raise "'tags' field of service '#{service_id.id}' must be an 'ArrayLiteral', got '#{tags.class_name.id}'."
59+
end %}
9860

9961
# Generic services are somewhat coupled to the annotation, so do a check here in addition to those in `ResolveGenerics`.
10062
{%
@@ -138,8 +100,8 @@ module Athena::DependencyInjection::ServiceContainer::RegisterServices
138100
shared: klass.class?,
139101
calls: calls,
140102
configurator: nil,
141-
tags: definition_tags,
142-
public: ann[:public] == true,
103+
tags: tags,
104+
public: ann["public"] == true,
143105
decorated_service: nil,
144106
bindings: {} of Nil => Nil,
145107
generics: ann.args,

src/service_container.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class Athena::DependencyInjection::ServiceContainer
5555
RegisterServices,
5656
ProcessAliases,
5757
ProcessAutoconfigureAnnotations,
58+
ProcessTags,
5859
ProcessParameters,
5960
ValidateGenerics,
6061
],

0 commit comments

Comments
 (0)