Class: Slayer::Service

Inherits:
Object
  • Object
show all
Defined in:
lib/slayer/service.rb

Overview

Slayer Services are objects that should implement re-usable pieces of application logic or common tasks. To prevent circular dependencies Services are required to declare which other Service classes they depend on. If a circular dependency is detected an error is raised.

In order to enforce the lack of circular dependencies, Service objects can only call other Services that are declared in their dependencies.

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.depsObject (readonly)

Returns the value of attribute deps


66
67
68
# File 'lib/slayer/service.rb', line 66

def deps
  @deps
end

Class Method Details

.after_each_methodObject


126
127
128
129
# File 'lib/slayer/service.rb', line 126

def after_each_method(*)
  @@allowed_services.pop
  @@allowed_services = nil if @@allowed_services.empty?
end

.before_each_methodObject


105
106
107
108
109
110
111
112
113
114
# File 'lib/slayer/service.rb', line 105

def before_each_method(*)
  @deps ||= []
  @@allowed_services ||= nil

  # Confirm that this method call is allowed
  raise_if_not_allowed

  @@allowed_services ||= []
  @@allowed_services << @deps
end

.dependencies(*deps) ⇒ Array<Class>

List the other Service class that this service class depends on. Only dependencies that are included in this call my be invoked from class or instances methods of this service class.

If no dependencies are provided, no other Service classes may be used by this Service class.

Examples:

Service calls with dependency declared

class StripeService < Slayer::Service
  dependencies NetworkService

  def self.pay()
    ...
    NetworkService.post(url: "stripe.com", body: my_payload) # OK
    ...
  end
end

Service calls without a dependency declared

class JiraApiService < Slayer::Service

  def self.create_issue()
    ...
    NetworkService.post(url: "stripe.com", body: my_payload) # Raises Slayer::ServiceDependencyError
    ...
  end
end

Raises:


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/slayer/service.rb', line 41

def self.dependencies(*deps)
  raise(ServiceDependencyError, "There were multiple dependencies calls of #{self}") if @deps

  deps.each do |dep|
    unless dep.is_a?(Class)
      raise(ServiceDependencyError, "The object #{dep} passed to dependencies service was not a class")
    end

    unless dep < Slayer::Service
      raise(ServiceDependencyError, "The object #{dep} passed to dependencies was not a subclass of #{self}")
    end
  end

  unless deps.uniq.length == deps.length
    raise(ServiceDependencyError, "There were duplicate dependencies in #{self}")
  end

  @deps = deps

  # Calculate the transitive dependencies and raise an error if there are circular dependencies
  transitive_dependencies
end

.method_added(name) ⇒ Object


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/slayer/service.rb', line 156

def method_added(name)
  return if self == Slayer::Service
  return if @__last_methods_added && @__last_methods_added.include?(name)

  with = :"#{name}_with_before_each_method"
  without = :"#{name}_without_before_each_method"

  @__last_methods_added = [name, with, without]
  define_method with do |*args, &block|
    self.class.before_each_method name
    begin
      send without, *args, &block
    rescue
      raise
    ensure
      self.class.after_each_method name
    end
  end

  alias_method without, name
  alias_method name, with

  @__last_methods_added = nil
end

.raise_if_not_allowedObject


116
117
118
119
120
121
122
123
124
# File 'lib/slayer/service.rb', line 116

def raise_if_not_allowed
  if @@allowed_services
    allowed = @@allowed_services.last
    if !allowed || !allowed.include?(self)
      raise(ServiceDependencyError, "Attempted to call #{self} from another #{Slayer::Service}"\
                                    ' which did not declare it as a dependency')
    end
  end
end

.singleton_method_added(name) ⇒ Object


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/slayer/service.rb', line 131

def singleton_method_added(name)
  return if self == Slayer::Service
  return if @__last_methods_added && @__last_methods_added.include?(name)

  with = :"#{name}_with_before_each_method"
  without = :"#{name}_without_before_each_method"

  @__last_methods_added = [name, with, without]
  define_singleton_method with do |*args, &block|
    before_each_method name
    begin
      send without, *args, &block
    rescue
      raise
    ensure
      after_each_method name
    end
  end

  singleton_class.send(:alias_method, without, name)
  singleton_class.send(:alias_method, name, with)

  @__last_methods_added = nil
end

.transitive_dependencies(dependency_hash = {}, visited = []) ⇒ Object


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/slayer/service.rb', line 68

def transitive_dependencies(dependency_hash = {}, visited = [])
  return @transitive_dependencies if @transitive_dependencies

  @deps ||= []

  # If we've already visited ourself, bail out. This is necessary to halt
  # execution for a circular chain of dependencies. #halting-problem-solved
  return dependency_hash[self] if visited.include?(self)

  visited << self
  dependency_hash[self] ||= []

  # Add each of our dependencies (and it's transitive dependency chain) to our
  # own dependencies.

  @deps.each do |dep|
    dependency_hash[self] << dep

    unless visited.include?(dep)
      child_transitive_dependencies = dep.transitive_dependencies(dependency_hash, visited)
      dependency_hash[self].concat(child_transitive_dependencies)
    end

    dependency_hash[self].uniq
  end

  # NO CIRCULAR DEPENDENCIES!
  if dependency_hash[self].include? self
    raise(ServiceDependencyError, "#{self} had a circular dependency")
  end

  # Store these now, so next time we can short-circuit.
  @transitive_dependencies = dependency_hash[self]

  return @transitive_dependencies
end