shamu 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +26 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +89 -30
  5. data/.yardopts +4 -5
  6. data/Gemfile +24 -12
  7. data/Guardfile +5 -0
  8. data/LABELS.md +22 -0
  9. data/README.md +41 -0
  10. data/Rakefile +12 -0
  11. data/circle.yml +7 -3
  12. data/config.ru +7 -0
  13. data/lib/shamu/active_record.rb +7 -0
  14. data/lib/shamu/attributes/assignment.rb +114 -0
  15. data/lib/shamu/attributes/equality.rb +40 -0
  16. data/lib/shamu/attributes/fluid_assignment.rb +49 -0
  17. data/lib/shamu/attributes/validation.rb +74 -0
  18. data/lib/shamu/attributes.rb +255 -0
  19. data/lib/shamu/auditing/README.md +0 -0
  20. data/lib/shamu/auditing/audit_record.rb +32 -0
  21. data/lib/shamu/auditing/auditing_service.rb +32 -0
  22. data/lib/shamu/auditing/list_scope.rb +22 -0
  23. data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
  24. data/lib/shamu/auditing/support.rb +75 -0
  25. data/lib/shamu/auditing/transaction.rb +58 -0
  26. data/lib/shamu/auditing.rb +12 -0
  27. data/lib/shamu/entities/README.md +1 -0
  28. data/lib/shamu/entities/active_record.rb +123 -0
  29. data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
  30. data/lib/shamu/entities/entity.rb +196 -0
  31. data/lib/shamu/entities/entity_path.rb +87 -0
  32. data/lib/shamu/entities/identity_cache.rb +64 -0
  33. data/lib/shamu/entities/list.rb +54 -0
  34. data/lib/shamu/entities/list_scope/dates.rb +57 -0
  35. data/lib/shamu/entities/list_scope/paging.rb +51 -0
  36. data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
  37. data/lib/shamu/entities/list_scope/sorting.rb +76 -0
  38. data/lib/shamu/entities/list_scope.rb +105 -0
  39. data/lib/shamu/entities/null_entity.rb +88 -0
  40. data/lib/shamu/entities.rb +11 -0
  41. data/lib/shamu/error.rb +23 -5
  42. data/lib/shamu/events/README.md +0 -0
  43. data/lib/shamu/events/active_record/channel.rb +36 -0
  44. data/lib/shamu/events/active_record/message.rb +52 -0
  45. data/lib/shamu/events/active_record/migration.rb +49 -0
  46. data/lib/shamu/events/active_record/runner.rb +28 -0
  47. data/lib/shamu/events/active_record/service.rb +174 -0
  48. data/lib/shamu/events/active_record.rb +13 -0
  49. data/lib/shamu/events/channel_stats.rb +23 -0
  50. data/lib/shamu/events/error.rb +24 -0
  51. data/lib/shamu/events/events_service.rb +136 -0
  52. data/lib/shamu/events/in_memory/async_service.rb +48 -0
  53. data/lib/shamu/events/in_memory/service.rb +97 -0
  54. data/lib/shamu/events/in_memory.rb +10 -0
  55. data/lib/shamu/events/message.rb +38 -0
  56. data/lib/shamu/events/support.rb +60 -0
  57. data/lib/shamu/events.rb +12 -0
  58. data/lib/shamu/features/README.md +0 -0
  59. data/lib/shamu/features/conditions/condition.rb +39 -0
  60. data/lib/shamu/features/conditions/env.rb +37 -0
  61. data/lib/shamu/features/conditions/hosts.rb +25 -0
  62. data/lib/shamu/features/conditions/matching.rb +16 -0
  63. data/lib/shamu/features/conditions/not_matching.rb +16 -0
  64. data/lib/shamu/features/conditions/percentage.rb +44 -0
  65. data/lib/shamu/features/conditions/proc.rb +54 -0
  66. data/lib/shamu/features/conditions/roles.rb +23 -0
  67. data/lib/shamu/features/conditions/schedule_at.rb +27 -0
  68. data/lib/shamu/features/conditions.rb +18 -0
  69. data/lib/shamu/features/config_service.rb +10 -0
  70. data/lib/shamu/features/context.rb +80 -0
  71. data/lib/shamu/features/env_store.rb +88 -0
  72. data/lib/shamu/features/errors.rb +29 -0
  73. data/lib/shamu/features/features_service.rb +168 -0
  74. data/lib/shamu/features/list_scope.rb +30 -0
  75. data/lib/shamu/features/selector.rb +50 -0
  76. data/lib/shamu/features/support.rb +51 -0
  77. data/lib/shamu/features/toggle.rb +149 -0
  78. data/lib/shamu/features/toggle_codec.rb +69 -0
  79. data/lib/shamu/features.rb +16 -0
  80. data/lib/shamu/locale/en.yml +22 -2
  81. data/lib/shamu/logger.rb +13 -0
  82. data/lib/shamu/rack/README.md +0 -0
  83. data/lib/shamu/rack/cookies.rb +115 -0
  84. data/lib/shamu/rack/cookies_middleware.rb +26 -0
  85. data/lib/shamu/rack/query_params.rb +41 -0
  86. data/lib/shamu/rack/query_params_middleware.rb +24 -0
  87. data/lib/shamu/rack.rb +12 -0
  88. data/lib/shamu/rails/controller.rb +131 -0
  89. data/lib/shamu/rails/entity.rb +168 -0
  90. data/lib/shamu/rails/features.rb +13 -0
  91. data/lib/shamu/rails/railtie.rb +30 -0
  92. data/lib/shamu/rails.rb +10 -0
  93. data/lib/shamu/rspec/matchers.rb +44 -0
  94. data/lib/shamu/rspec.rb +1 -0
  95. data/lib/shamu/security/README.md +0 -0
  96. data/lib/shamu/security/active_record_policy.rb +106 -0
  97. data/lib/shamu/security/error.rb +65 -0
  98. data/lib/shamu/security/hashed_value.rb +71 -0
  99. data/lib/shamu/security/no_policy.rb +15 -0
  100. data/lib/shamu/security/policy.rb +289 -0
  101. data/lib/shamu/security/policy_refinement.rb +50 -0
  102. data/lib/shamu/security/policy_rule.rb +59 -0
  103. data/lib/shamu/security/principal.rb +72 -0
  104. data/lib/shamu/security/roles.rb +62 -0
  105. data/lib/shamu/security/roles_service.rb +30 -0
  106. data/lib/shamu/security/support.rb +83 -0
  107. data/lib/shamu/security.rb +43 -0
  108. data/lib/shamu/services/README.md +2 -0
  109. data/lib/shamu/services/active_record.rb +58 -0
  110. data/lib/shamu/services/active_record_crud.rb +378 -0
  111. data/lib/shamu/services/error.rb +24 -0
  112. data/lib/shamu/services/lazy_association.rb +31 -0
  113. data/lib/shamu/services/lazy_transform.rb +97 -0
  114. data/lib/shamu/services/request.rb +122 -0
  115. data/lib/shamu/services/request_support.rb +124 -0
  116. data/lib/shamu/services/result.rb +75 -0
  117. data/lib/shamu/services/service.rb +355 -0
  118. data/lib/shamu/services.rb +12 -0
  119. data/lib/shamu/sessions/README.md +2 -0
  120. data/lib/shamu/sessions/cookie_store.rb +79 -0
  121. data/lib/shamu/sessions/session_store.rb +42 -0
  122. data/lib/shamu/sessions.rb +8 -0
  123. data/lib/shamu/to_bool_extension.rb +57 -0
  124. data/lib/shamu/to_model_id_extension.rb +50 -0
  125. data/lib/shamu/version.rb +10 -4
  126. data/lib/shamu.rb +18 -6
  127. data/shamu.gemspec +21 -10
  128. data/spec/internal/README.md +4 -0
  129. data/spec/internal/config/database.yml +3 -0
  130. data/spec/internal/config/routes.rb +3 -0
  131. data/spec/internal/db/schema.rb +3 -0
  132. data/spec/internal/log/.gitignore +1 -0
  133. data/spec/internal/public/favicon.ico +0 -0
  134. data/spec/lib/shamu/active_record_support.rb +32 -0
  135. data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
  136. data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
  137. data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
  138. data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
  139. data/spec/lib/shamu/attributes_spec.rb +331 -0
  140. data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
  141. data/spec/lib/shamu/auditing/support_spec.rb +41 -0
  142. data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
  143. data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
  144. data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
  145. data/spec/lib/shamu/entities/entity_spec.rb +56 -0
  146. data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
  147. data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
  148. data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
  149. data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
  150. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
  151. data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
  152. data/spec/lib/shamu/entities/list_spec.rb +60 -0
  153. data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
  154. data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
  155. data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
  156. data/spec/lib/shamu/events/events_service_spec.rb +57 -0
  157. data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
  158. data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
  159. data/spec/lib/shamu/events/message_spec.rb +7 -0
  160. data/spec/lib/shamu/events/support_spec.rb +44 -0
  161. data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
  162. data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
  163. data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
  164. data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
  165. data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
  166. data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
  167. data/spec/lib/shamu/features/env_store_spec.rb +48 -0
  168. data/spec/lib/shamu/features/features.yml +34 -0
  169. data/spec/lib/shamu/features/features_service_spec.rb +109 -0
  170. data/spec/lib/shamu/features/secondary.yml +5 -0
  171. data/spec/lib/shamu/features/selector_spec.rb +17 -0
  172. data/spec/lib/shamu/features/support_spec.rb +45 -0
  173. data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
  174. data/spec/lib/shamu/features/toggle_spec.rb +42 -0
  175. data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
  176. data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
  177. data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
  178. data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
  179. data/spec/lib/shamu/rails/controller_spec.rb +74 -0
  180. data/spec/lib/shamu/rails/entity_spec.rb +150 -0
  181. data/spec/lib/shamu/rails/features.yml +13 -0
  182. data/spec/lib/shamu/rails/features_spec.rb +45 -0
  183. data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
  184. data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
  185. data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
  186. data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
  187. data/spec/lib/shamu/security/policy_spec.rb +158 -0
  188. data/spec/lib/shamu/security/roles_spec.rb +46 -0
  189. data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
  190. data/spec/lib/shamu/services/active_record_spec.rb +92 -0
  191. data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
  192. data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
  193. data/spec/lib/shamu/services/request_spec.rb +58 -0
  194. data/spec/lib/shamu/services/request_support_spec.rb +129 -0
  195. data/spec/lib/shamu/services/result_spec.rb +37 -0
  196. data/spec/lib/shamu/services/service_spec.rb +307 -0
  197. data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
  198. data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
  199. data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
  200. data/spec/rails_helper.rb +13 -0
  201. data/spec/spec_helper.rb +17 -12
  202. data/spec/support/active_record.rb +17 -0
  203. data/spec/support/database.rb +14 -0
  204. data/spec/support/logger.rb +0 -0
  205. metadata +383 -9
  206. data/spec/lib/shamu_spec.rb +0 -5
  207. /data/{spec/internal/log/test.log → lib/shamu/attributes/README.md} +0 -0
@@ -0,0 +1,76 @@
1
+ module Shamu
2
+ module Entities
3
+ class ListScope
4
+
5
+ # Include sorting parameters and parsing.
6
+ #
7
+ # ```
8
+ # class UsersListScope < Shamu::Entities::ListScope
9
+ # include Shamu::Entities::ListScope::Sorting
10
+ # end
11
+ #
12
+ # scope = UserListScope.coerce!( sort_by: { first_name: :desc } )
13
+ # scope.sort_by #=> { first_name: :desc }
14
+ #
15
+ # scope = UserListScope.coerce!( sort_by: :first_name )
16
+ # scope.sort_by #=> { first_name: :asc }
17
+ #
18
+ # scope = UserListScope.coerce!( sort_by: [ :first_name, :last_name ] )
19
+ # scope.sort_by #=> { first_name: :asc, last_name: :asc }
20
+ # ```
21
+ module Sorting
22
+
23
+ # ============================================================================
24
+ # @!group Attributes
25
+ #
26
+
27
+ # @!attribute sort_by
28
+ # @return [Hash] the attributes and directions to sort by.
29
+ #
30
+ # The sort attribute is coerced by converting arrays to a hash with a
31
+ # default direction of :asc for each attribute.
32
+ #
33
+ # ```
34
+ # scope.sort_by :name # => { name: :asc }
35
+ # scope.sort_by :name, :created_at # => { name: :asc, created_at: :asc }
36
+ # scope.sort_by :count, rating: :desc # => { count: :asc, rating: :desc }
37
+ # ```
38
+
39
+ #
40
+ # @!endgroup Attributes
41
+
42
+ def self.included( base )
43
+ super
44
+
45
+ base.attribute :sort_by, coerce: ->( *values ) { parse_sort_by( values ) }
46
+ end
47
+
48
+ # @return [Boolean] true if the scope is paged.
49
+ def sorted?
50
+ !!sort_by
51
+ end
52
+
53
+ private
54
+
55
+ def parse_sort_by( arguments )
56
+ Array( arguments ).each_with_object( {} ) do |arg, sorted|
57
+ case arg
58
+ when Array then sorted.merge!( parse_sort_by( arg ) )
59
+ when Hash then
60
+ arg.each do |attr, direction|
61
+ case direction
62
+ when :asc, :desc, "asc", "desc" then sorted[attr] = direction.to_sym
63
+ when Array, Hash then sorted[attr] = parse_sort_by( direction )
64
+ else fail ArgumentError
65
+ end
66
+ end
67
+ when String, Symbol then sorted[arg.to_sym] = :asc
68
+ else fail ArgumentError
69
+ end
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,105 @@
1
+ module Shamu
2
+ module Entities
3
+
4
+ # The desired scope of entities offered {Services::Service} to prepare a
5
+ # list of {Entity entities}.
6
+ #
7
+ # ### Standard scopes
8
+ #
9
+ # - {Paging}
10
+ # - {ScopedPaging}
11
+ # - {Dates}
12
+ # - {Sorting}
13
+ #
14
+ # @example
15
+ # class UsersListScope < Shamu::Entities::ListScope
16
+ #
17
+ # # Include standard paging options (page, per_page) from ListScopes::Paging
18
+ # include Shamu::Entities::ListScope::Paging
19
+ #
20
+ # # Allow client to request that users be limited to those in one of the
21
+ # # given roles.
22
+ # attribute :roles, array: true, coerce: :to_s
23
+ # end
24
+ class ListScope
25
+ include Attributes
26
+ include Attributes::Assignment
27
+ include Attributes::FluidAssignment
28
+ include Attributes::Validation
29
+
30
+ require "shamu/entities/list_scope/paging"
31
+ require "shamu/entities/list_scope/scoped_paging"
32
+ require "shamu/entities/list_scope/dates"
33
+ require "shamu/entities/list_scope/sorting"
34
+
35
+
36
+ # Clone the params but exclude the given parameters.
37
+ # @param [Array<Symbol>] param_names to exclude.
38
+ # @return [ListScope]
39
+ def except( *param_names )
40
+ self.class.new( to_attributes( except: param_names ) )
41
+ end
42
+
43
+ # @return [Hash] the hash of attributes that can be used to generate a url.
44
+ def params
45
+ params = to_attributes
46
+ params.each do |key, value|
47
+ params[key] = value.params if value.respond_to?( :params )
48
+ end
49
+ params
50
+ end
51
+
52
+ class << self
53
+ # Coerces a hash or params object to a proper ListScope object.
54
+ # @param [Object] params to be coerced.
55
+ # @return [ListScope] the coerced scope
56
+ def coerce( params )
57
+ if params.is_a?( self )
58
+ params
59
+ elsif params.respond_to?( :to_h ) || params.respond_to?( :to_attributes )
60
+ new( params )
61
+ elsif params.nil?
62
+ new
63
+ else
64
+ raise ArgumentError
65
+ end
66
+ end
67
+
68
+ # Coerces the given params object and raises an ArgumentError if any of
69
+ # the parameters are invalid.
70
+ # @param (see .coerce)
71
+ # @return (see .coerce)
72
+ def coerce!( params )
73
+ coerced = coerce( params )
74
+ raise ArgumentError unless coerced.valid?
75
+ coerced
76
+ end
77
+
78
+ # Finds the natural {ListScope} class for the given entity class.
79
+ #
80
+ # Users::UserEntity -> Users::UserListScope or Users::ListScope
81
+ #
82
+ # @param [Class] entity_class the {Entity} class to find a scope for.
83
+ # @return [ListScope] the custom list scope if found, otherwise
84
+ # {ListScope}.
85
+ def for( entity_class )
86
+ base_name = entity_class.name || "Entity"
87
+ name = base_name.sub /(Entity)?$/, "ListScope"
88
+ begin
89
+ return name.constantize
90
+ rescue NameError # rubocop:disable Lint/HandleExceptions
91
+ end
92
+
93
+ name = base_name.sub /::[A-Za-z0-9]+(Entity)?$/, "::ListScope"
94
+
95
+ begin
96
+ return name.constantize
97
+ rescue NameError # rubocop:disable Lint/HandleExceptions
98
+ end
99
+
100
+ self
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,88 @@
1
+ module Shamu
2
+ module Entities
3
+
4
+ # Null entities look at feel just like their natural counterparts but are
5
+ # not backed by any real data. Rather than returning null from a service
6
+ # lookup function, services will return a null entity so that clients do not
7
+ # need to constantly check for nil before formatting output.
8
+ #
9
+ # ```
10
+ # class UserEntity < Entity
11
+ # attribute :name
12
+ # attribute :id
13
+ # attribute :email
14
+ # end
15
+ #
16
+ # class NullUserEntity < UserEntity
17
+ # include NullEntity
18
+ # end
19
+ #
20
+ # user = user_service.lookup( real_user_id )
21
+ # user # => UserEntity
22
+ # user.name # => "Shamu"
23
+ # user.email # => "start@seaworld.com"
24
+ # user.id # => 5
25
+ #
26
+ # user = user_service.lookup( unknown_user_id )
27
+ # user # => NullUserEntity
28
+ # user.name # => "Unknown User"
29
+ # user.email # => nil
30
+ # user.id # => nil
31
+ # ```
32
+ module NullEntity
33
+
34
+ # Attributes to automatically format as "Unknown {Entity Class Name}"
35
+ AUTO_FORMATTED_ATTRIBUTES = %i( name title label ).freeze
36
+
37
+ # @return [nil]
38
+ # Prevent rails url helpers from generating URLs for the entity.
39
+ def to_param
40
+ end
41
+
42
+ # @return [true]
43
+ #
44
+ # Allow clients to adjust behavior if needed for missing entities.
45
+ def empty?
46
+ true
47
+ end
48
+
49
+ def self.included( base )
50
+ AUTO_FORMATTED_ATTRIBUTES.each do |attr|
51
+ next unless base.attributes.key?( attr )
52
+
53
+ base_name ||= begin
54
+ name = base.name || "Resource"
55
+ name.split( "::" )
56
+ .last
57
+ .sub( /Entity/, "" )
58
+ .gsub( /(.)([[:upper:]])/, '\1 \2' )
59
+ end
60
+ base.attribute attr, default: "Unknown #{ base_name }"
61
+ end
62
+ end
63
+
64
+ # Dynamically generate a new null entity class.
65
+ # @param [Class] entity_class {Entity} class
66
+ # @return [Class] a null entity class derived from `entity_class`.
67
+ def self.for( entity_class )
68
+ if null_klass = ( entity_class.const_defined?( :NullEntity, false ) &&
69
+ entity_class.const_get( :NullEntity, false ) )
70
+ # If the base class is reloaded a-la rails dev, then regenerate the
71
+ # null class as well.
72
+ null_klass = nil if null_klass.superclass != entity_class
73
+ end
74
+
75
+ unless null_klass
76
+ null_klass = Class.new( entity_class ) do
77
+ include ::Shamu::Entities::NullEntity
78
+ end
79
+
80
+ entity_class.const_set :NullEntity, null_klass
81
+ end
82
+
83
+ null_klass
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,11 @@
1
+ module Shamu
2
+ # {include:file:lib/shamu/entities/README.md}
3
+ module Entities
4
+ require "shamu/entities/entity"
5
+ require "shamu/entities/null_entity"
6
+ require "shamu/entities/list"
7
+ require "shamu/entities/list_scope"
8
+ require "shamu/entities/identity_cache"
9
+ require "shamu/entities/entity_path"
10
+ end
11
+ end
data/lib/shamu/error.rb CHANGED
@@ -1,13 +1,31 @@
1
- require 'i18n'
1
+ require "i18n"
2
2
 
3
- module Schamu
4
- class Error < StandardError
3
+ module Shamu
5
4
 
5
+ # A generic error class for problems in the shamu library.
6
+ class Error < StandardError
6
7
  private
7
- def translate( key, args = {} )
8
- I18n.translate key, args.merge( scope: [:shamu,:errors,:messages] )
8
+
9
+ def translation_scope
10
+ [ :shamu, :errors ]
11
+ end
12
+
13
+ def translate( key, **args )
14
+ I18n.translate key, args.merge( scope: translation_scope )
9
15
  end
10
16
  end
11
17
 
18
+ # The resource was not found.
19
+ class NotFoundError < Error
20
+ def initialize( message = :not_found )
21
+ super translate( message )
22
+ end
23
+ end
12
24
 
25
+ # The method is not implemented.
26
+ class NotImplementedError < Error
27
+ def initialize( message = :not_implemented )
28
+ super translate( message )
29
+ end
30
+ end
13
31
  end
File without changes
@@ -0,0 +1,36 @@
1
+ module Shamu
2
+ module Events
3
+ module ActiveRecord
4
+
5
+ # Registry of event channels.
6
+ class Channel < ::ActiveRecord::Base
7
+
8
+ self.table_name = "shamu_event_channels"
9
+
10
+ # ============================================================================
11
+ # @!group Attributes
12
+ #
13
+
14
+ # @!attribute
15
+ # @return [String] name of the channel.
16
+
17
+ #
18
+ # @!endgroup Attributes
19
+
20
+ # ============================================================================
21
+ # @!group Scope
22
+ #
23
+
24
+ # @!attribute
25
+ # @return [ActiveRecord::Relation] messages posted to the given channel.
26
+ scope :by_name, ->( name ) {
27
+ where( name: name )
28
+ }
29
+
30
+ #
31
+ # @!endgroup Scope
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ module Shamu
2
+ module Events
3
+ module ActiveRecord
4
+
5
+ # The model used to store the event messages in the database.
6
+ class Message < ::ActiveRecord::Base
7
+
8
+ self.table_name = "shamu_event_messages"
9
+ self.primary_key = "id"
10
+
11
+ # ============================================================================
12
+ # @!group Attributes
13
+ #
14
+
15
+ # @!attribute
16
+ # @return [String] id of the message a UUID.
17
+
18
+ # @!attribute
19
+ # @return [Integer] channel_id
20
+
21
+ # @!attribute
22
+ # @return [String] message the serialized message.
23
+
24
+ # @!attribute
25
+ # @return [DateTime] timestamp when the event was submitted.
26
+
27
+ #
28
+ # @!endgroup Attributes
29
+
30
+ # ============================================================================
31
+ # @!group Scope
32
+ #
33
+
34
+ # @!attribute
35
+ # @return [ActiveRecord::Relation] messages posted to the given channel.
36
+ scope :by_channel, ->( name ) {
37
+ where( channel: name )
38
+ }
39
+
40
+ # @!attribute
41
+ # @return [ActiveRecord::Relation] messages posted after the given created_at.
42
+ scope :since, ->( created_at ) {
43
+ where( arel_table[:created_at].gt( created_at ) )
44
+ }
45
+
46
+ #
47
+ # @!endgroup Scope
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,49 @@
1
+ module Shamu
2
+ module Events
3
+ module ActiveRecord
4
+
5
+ # Prepare the database for storing event messages.
6
+ class Migration < ::ActiveRecord::Migration
7
+
8
+ self.verbose = false
9
+
10
+ # rubocop:disable Metrics/MethodLength
11
+
12
+ def up
13
+ return if table_exists? Message.table_name
14
+
15
+ # TODO: Need to provide a means for using 64-bit primary keys in
16
+ # databases that support it. Otherwise limited to 4B events.
17
+ create_table Message.table_name do |t|
18
+ t.integer :channel_id, null: false
19
+ t.string :message, null: false
20
+
21
+ t.index :id
22
+ t.index :channel_id
23
+ end
24
+
25
+ create_table Channel.table_name do |t|
26
+ t.string :name, null: false, unique: true
27
+
28
+ t.index :name
29
+ end
30
+
31
+ create_table Runner.table_name, id: false do |t|
32
+ t.timestamp :last_processed_at
33
+ t.integer :last_processed_id
34
+ t.string :id, null: false
35
+
36
+ t.index :id, unique: true
37
+ end
38
+ end
39
+
40
+ def down
41
+ drop_table Message.table_name if table_exists? Message.table_name
42
+ drop_table Channel.table_name if table_exists? Channel.table_name
43
+ drop_table Runner.table_name if table_exists? Runner.table_name
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,28 @@
1
+ module Shamu
2
+ module Events
3
+ module ActiveRecord
4
+
5
+ # Keep track of the last time message processed by a channel dispatch
6
+ # runner.
7
+ class Runner < ::ActiveRecord::Base
8
+
9
+ self.table_name = "shamu_event_runners"
10
+ self.primary_key = "id"
11
+
12
+ # ============================================================================
13
+ # @!group Attributes
14
+ #
15
+
16
+ # @!attribute id
17
+ # @return [String] the runner's UUID.
18
+
19
+ # @!attribute last_processed_timestamp
20
+ # @return [Datetime] timestamp of the last message processed.
21
+
22
+ #
23
+ # @!endgroup Attributes
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,174 @@
1
+ require "thread"
2
+
3
+ module Shamu
4
+ module Events
5
+ module ActiveRecord
6
+
7
+ # Store events in a database using ActiveRecord persistence layer.
8
+ #
9
+ # ## Runner IDS
10
+ #
11
+ # A globally unique id (may be UUID or a well- defined internal
12
+ # convention that guarantees uniqueness.) The runner id is used by the
13
+ # system to track which messages have been delivered to the subscribers
14
+ # hosted by that runner process. This allows dispatching to resume should
15
+ # the host or process die.
16
+ class Service < EventsService
17
+ include ChannelStats
18
+
19
+ # Ensure that the tables are present in the database and have been
20
+ # initialized.
21
+ #
22
+ # @return [void]
23
+ def self.ensure_records!
24
+ return if @ensure_records
25
+
26
+ @ensure_records = true
27
+ Migration.new.migrate( :up )
28
+ end
29
+
30
+ initialize do
31
+ self.class.ensure_records!
32
+ @channels ||= {}
33
+ @mutex ||= Mutex.new
34
+ end
35
+
36
+ # (see EventsService#publish)
37
+ def publish( channel, message )
38
+ channel_id = fetch_channel( channel )[:id]
39
+ Message.create! channel_id: channel_id, message: serialize( message )
40
+ end
41
+
42
+ # (see EventsService#subscribe)
43
+ def subscribe( channel, &callback )
44
+ state = fetch_channel( channel )
45
+ mutex.synchronize do
46
+ state[:subscribers] << callback
47
+ end
48
+ end
49
+
50
+ # Dispatch queued messages up to the given `limit`. Once all the
51
+ # messages are dispatched, the method returns. A long running process
52
+ # might periodically call dispatch in a loop trapping SIGINT to
53
+ # shutdown.
54
+ #
55
+ # @param [String] runner_id that identifies the host and process
56
+ # responding to events.
57
+ # @param [Array<String>] names of the channels to dispatch. If empty,
58
+ # dispatches to all subscribed channels.
59
+ # @param [Integer] limit the maximum number of messages to dispatch. If
60
+ # not given, defaults to 100.
61
+ #
62
+ # @return [Hash<String,Integer>] the number of messages actually
63
+ # dispatched on each channel.
64
+ def dispatch( runner_id, *names, limit: nil )
65
+ fail UnknownRunnerError unless runner_id.present?
66
+ names = channels.keys unless channels.present?
67
+
68
+ names.each_with_object( {} ) do |name, dispatched|
69
+ state = fetch_channel( name )
70
+ dispatched[name] = dispatch_channel( state, "#{ runner_id }::#{ name }", limit )
71
+ end
72
+ end
73
+
74
+ # (see ChannelStats#channel_stats)
75
+ # @param [String] runner_id if provided, only show stats for the given runner.
76
+ def channel_stats( name, runner_id: nil )
77
+ channel = fetch_channel( name )
78
+ queue = Message.where( channel_id: channel[:id] )
79
+
80
+ if runner_id && ( runner = create_runner( runner_id ) )
81
+ if runner.last_processed_id
82
+ queue = queue.where( Message.arel_table[ :id ].gt( runner.last_processed_id ) )
83
+ end
84
+ end
85
+
86
+ {
87
+ name: name,
88
+ subscribers_count: channel[:subscribers].size,
89
+ dispatching: channel[:dispatching],
90
+ queue_size: queue.count
91
+ }
92
+ end
93
+
94
+ private
95
+
96
+ attr_reader :channels
97
+ attr_reader :mutex
98
+
99
+ def create_channel( name )
100
+ {
101
+ id: create_named_channel( name ).id,
102
+ subscribers: []
103
+ }
104
+ end
105
+
106
+ def dispatch_channel( state, runner_id, limit )
107
+ mutex.synchronize do
108
+ return if state[:dispatching]
109
+ state[ :dispatching ] = true
110
+ end
111
+
112
+ dispatch_messages( state, runner_id, limit )
113
+
114
+ ensure
115
+ mutex.synchronize do
116
+ state[ :dispatching ] = false
117
+ end
118
+ end
119
+
120
+ def dispatch_messages( state, runner_id, limit )
121
+ last_message = nil
122
+ count = 0
123
+
124
+ pending_messages( state, runner_id, limit ).each do |record|
125
+ last_message = record
126
+ message = deserialize( record.message )
127
+
128
+ count += 1
129
+
130
+ state[ :subscribers ].each do |subscriber|
131
+ subscriber.call( message )
132
+ end
133
+ end
134
+
135
+ bookmark_runner( runner_id, last_message )
136
+
137
+ count
138
+ end
139
+
140
+ def bookmark_runner( runner_id, last_message )
141
+ return unless last_message
142
+
143
+ runner = create_runner( runner_id )
144
+ runner.update_attributes last_processed_id: last_message.id, last_processed_at: Time.now.utc
145
+ end
146
+
147
+ def pending_messages( state, runner_id, limit )
148
+ messages = Message.where( channel_id: state[:id] )
149
+ .limit( limit )
150
+ runner = create_runner( runner_id )
151
+
152
+ if runner.last_processed_id
153
+ messages = messages.where( Message.arel_table[:id].gt( runner.last_processed_id ) )
154
+ end
155
+
156
+ messages
157
+ end
158
+
159
+ def create_runner( runner_id )
160
+ Runner.transaction( requires_new: true ) do
161
+ Runner.first_or_create!( id: runner_id )
162
+ end
163
+ end
164
+
165
+ def create_named_channel( name )
166
+ Channel.transaction( requires_new: true ) do
167
+ Channel.first_or_create!( name: name )
168
+ end
169
+ end
170
+
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,13 @@
1
+ module Shamu
2
+ module Events
3
+
4
+ # See {ActiveRecord::Service}
5
+ module ActiveRecord
6
+ require "shamu/events/active_record/service"
7
+ require "shamu/events/active_record/message"
8
+ require "shamu/events/active_record/channel"
9
+ require "shamu/events/active_record/runner"
10
+ require "shamu/events/active_record/migration"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module Shamu
2
+ module Events
3
+
4
+ # Indicates that an {EventsService} supports reporting channel activity states.
5
+ module ChannelStats
6
+
7
+ # Gets stats for the given `channel`.
8
+ #
9
+ # #### Stats Included in the results.
10
+ #
11
+ # - **name** name of the channel.
12
+ # - **subscribers_count** the number of subscribers.
13
+ # - **queue_size** the size of the message queue.
14
+ # - **dispatching** true if the channel is currently dispatching messages.
15
+ #
16
+ # @param [String] name of the channel
17
+ # @return [Hash] stats.
18
+ def channel_stats( name )
19
+ fail NotImplementedError
20
+ end
21
+ end
22
+ end
23
+ end