shamu 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +26 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +89 -30
- data/.yardopts +4 -5
- data/Gemfile +24 -12
- data/Guardfile +5 -0
- data/LABELS.md +22 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/circle.yml +7 -3
- data/config.ru +7 -0
- data/lib/shamu/active_record.rb +7 -0
- data/lib/shamu/attributes/assignment.rb +114 -0
- data/lib/shamu/attributes/equality.rb +40 -0
- data/lib/shamu/attributes/fluid_assignment.rb +49 -0
- data/lib/shamu/attributes/validation.rb +74 -0
- data/lib/shamu/attributes.rb +255 -0
- data/lib/shamu/auditing/README.md +0 -0
- data/lib/shamu/auditing/audit_record.rb +32 -0
- data/lib/shamu/auditing/auditing_service.rb +32 -0
- data/lib/shamu/auditing/list_scope.rb +22 -0
- data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
- data/lib/shamu/auditing/support.rb +75 -0
- data/lib/shamu/auditing/transaction.rb +58 -0
- data/lib/shamu/auditing.rb +12 -0
- data/lib/shamu/entities/README.md +1 -0
- data/lib/shamu/entities/active_record.rb +123 -0
- data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
- data/lib/shamu/entities/entity.rb +196 -0
- data/lib/shamu/entities/entity_path.rb +87 -0
- data/lib/shamu/entities/identity_cache.rb +64 -0
- data/lib/shamu/entities/list.rb +54 -0
- data/lib/shamu/entities/list_scope/dates.rb +57 -0
- data/lib/shamu/entities/list_scope/paging.rb +51 -0
- data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
- data/lib/shamu/entities/list_scope/sorting.rb +76 -0
- data/lib/shamu/entities/list_scope.rb +105 -0
- data/lib/shamu/entities/null_entity.rb +88 -0
- data/lib/shamu/entities.rb +11 -0
- data/lib/shamu/error.rb +23 -5
- data/lib/shamu/events/README.md +0 -0
- data/lib/shamu/events/active_record/channel.rb +36 -0
- data/lib/shamu/events/active_record/message.rb +52 -0
- data/lib/shamu/events/active_record/migration.rb +49 -0
- data/lib/shamu/events/active_record/runner.rb +28 -0
- data/lib/shamu/events/active_record/service.rb +174 -0
- data/lib/shamu/events/active_record.rb +13 -0
- data/lib/shamu/events/channel_stats.rb +23 -0
- data/lib/shamu/events/error.rb +24 -0
- data/lib/shamu/events/events_service.rb +136 -0
- data/lib/shamu/events/in_memory/async_service.rb +48 -0
- data/lib/shamu/events/in_memory/service.rb +97 -0
- data/lib/shamu/events/in_memory.rb +10 -0
- data/lib/shamu/events/message.rb +38 -0
- data/lib/shamu/events/support.rb +60 -0
- data/lib/shamu/events.rb +12 -0
- data/lib/shamu/features/README.md +0 -0
- data/lib/shamu/features/conditions/condition.rb +39 -0
- data/lib/shamu/features/conditions/env.rb +37 -0
- data/lib/shamu/features/conditions/hosts.rb +25 -0
- data/lib/shamu/features/conditions/matching.rb +16 -0
- data/lib/shamu/features/conditions/not_matching.rb +16 -0
- data/lib/shamu/features/conditions/percentage.rb +44 -0
- data/lib/shamu/features/conditions/proc.rb +54 -0
- data/lib/shamu/features/conditions/roles.rb +23 -0
- data/lib/shamu/features/conditions/schedule_at.rb +27 -0
- data/lib/shamu/features/conditions.rb +18 -0
- data/lib/shamu/features/config_service.rb +10 -0
- data/lib/shamu/features/context.rb +80 -0
- data/lib/shamu/features/env_store.rb +88 -0
- data/lib/shamu/features/errors.rb +29 -0
- data/lib/shamu/features/features_service.rb +168 -0
- data/lib/shamu/features/list_scope.rb +30 -0
- data/lib/shamu/features/selector.rb +50 -0
- data/lib/shamu/features/support.rb +51 -0
- data/lib/shamu/features/toggle.rb +149 -0
- data/lib/shamu/features/toggle_codec.rb +69 -0
- data/lib/shamu/features.rb +16 -0
- data/lib/shamu/locale/en.yml +22 -2
- data/lib/shamu/logger.rb +13 -0
- data/lib/shamu/rack/README.md +0 -0
- data/lib/shamu/rack/cookies.rb +115 -0
- data/lib/shamu/rack/cookies_middleware.rb +26 -0
- data/lib/shamu/rack/query_params.rb +41 -0
- data/lib/shamu/rack/query_params_middleware.rb +24 -0
- data/lib/shamu/rack.rb +12 -0
- data/lib/shamu/rails/controller.rb +131 -0
- data/lib/shamu/rails/entity.rb +168 -0
- data/lib/shamu/rails/features.rb +13 -0
- data/lib/shamu/rails/railtie.rb +30 -0
- data/lib/shamu/rails.rb +10 -0
- data/lib/shamu/rspec/matchers.rb +44 -0
- data/lib/shamu/rspec.rb +1 -0
- data/lib/shamu/security/README.md +0 -0
- data/lib/shamu/security/active_record_policy.rb +106 -0
- data/lib/shamu/security/error.rb +65 -0
- data/lib/shamu/security/hashed_value.rb +71 -0
- data/lib/shamu/security/no_policy.rb +15 -0
- data/lib/shamu/security/policy.rb +289 -0
- data/lib/shamu/security/policy_refinement.rb +50 -0
- data/lib/shamu/security/policy_rule.rb +59 -0
- data/lib/shamu/security/principal.rb +72 -0
- data/lib/shamu/security/roles.rb +62 -0
- data/lib/shamu/security/roles_service.rb +30 -0
- data/lib/shamu/security/support.rb +83 -0
- data/lib/shamu/security.rb +43 -0
- data/lib/shamu/services/README.md +2 -0
- data/lib/shamu/services/active_record.rb +58 -0
- data/lib/shamu/services/active_record_crud.rb +378 -0
- data/lib/shamu/services/error.rb +24 -0
- data/lib/shamu/services/lazy_association.rb +31 -0
- data/lib/shamu/services/lazy_transform.rb +97 -0
- data/lib/shamu/services/request.rb +122 -0
- data/lib/shamu/services/request_support.rb +124 -0
- data/lib/shamu/services/result.rb +75 -0
- data/lib/shamu/services/service.rb +355 -0
- data/lib/shamu/services.rb +12 -0
- data/lib/shamu/sessions/README.md +2 -0
- data/lib/shamu/sessions/cookie_store.rb +79 -0
- data/lib/shamu/sessions/session_store.rb +42 -0
- data/lib/shamu/sessions.rb +8 -0
- data/lib/shamu/to_bool_extension.rb +57 -0
- data/lib/shamu/to_model_id_extension.rb +50 -0
- data/lib/shamu/version.rb +10 -4
- data/lib/shamu.rb +18 -6
- data/shamu.gemspec +21 -10
- data/spec/internal/README.md +4 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/shamu/active_record_support.rb +32 -0
- data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
- data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
- data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
- data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
- data/spec/lib/shamu/attributes_spec.rb +331 -0
- data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
- data/spec/lib/shamu/auditing/support_spec.rb +41 -0
- data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
- data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
- data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
- data/spec/lib/shamu/entities/entity_spec.rb +56 -0
- data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
- data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
- data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
- data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
- data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
- data/spec/lib/shamu/entities/list_spec.rb +60 -0
- data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
- data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
- data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
- data/spec/lib/shamu/events/events_service_spec.rb +57 -0
- data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
- data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
- data/spec/lib/shamu/events/message_spec.rb +7 -0
- data/spec/lib/shamu/events/support_spec.rb +44 -0
- data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
- data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
- data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
- data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
- data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
- data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
- data/spec/lib/shamu/features/env_store_spec.rb +48 -0
- data/spec/lib/shamu/features/features.yml +34 -0
- data/spec/lib/shamu/features/features_service_spec.rb +109 -0
- data/spec/lib/shamu/features/secondary.yml +5 -0
- data/spec/lib/shamu/features/selector_spec.rb +17 -0
- data/spec/lib/shamu/features/support_spec.rb +45 -0
- data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
- data/spec/lib/shamu/features/toggle_spec.rb +42 -0
- data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
- data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
- data/spec/lib/shamu/rails/controller_spec.rb +74 -0
- data/spec/lib/shamu/rails/entity_spec.rb +150 -0
- data/spec/lib/shamu/rails/features.yml +13 -0
- data/spec/lib/shamu/rails/features_spec.rb +45 -0
- data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
- data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
- data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
- data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
- data/spec/lib/shamu/security/policy_spec.rb +158 -0
- data/spec/lib/shamu/security/roles_spec.rb +46 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
- data/spec/lib/shamu/services/active_record_spec.rb +92 -0
- data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
- data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
- data/spec/lib/shamu/services/request_spec.rb +58 -0
- data/spec/lib/shamu/services/request_support_spec.rb +129 -0
- data/spec/lib/shamu/services/result_spec.rb +37 -0
- data/spec/lib/shamu/services/service_spec.rb +307 -0
- data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
- data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
- data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
- data/spec/rails_helper.rb +13 -0
- data/spec/spec_helper.rb +17 -12
- data/spec/support/active_record.rb +17 -0
- data/spec/support/database.rb +14 -0
- data/spec/support/logger.rb +0 -0
- metadata +383 -9
- data/spec/lib/shamu_spec.rb +0 -5
- /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
|
1
|
+
require "i18n"
|
2
2
|
|
3
|
-
module
|
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
|
-
|
8
|
-
|
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
|