shamu 0.0.13 → 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +55 -20
- data/Gemfile +3 -3
- data/Gemfile.lock +13 -11
- data/circle.yml +1 -1
- data/lib/shamu/attributes/assignment.rb +44 -5
- data/lib/shamu/attributes/camel_case.rb +21 -0
- data/lib/shamu/attributes/validation.rb +13 -1
- data/lib/shamu/attributes/validators/valid_validator.rb +14 -0
- data/lib/shamu/attributes/validators.rb +7 -0
- data/lib/shamu/attributes.rb +13 -8
- data/lib/shamu/auditing/auditing_service.rb +1 -5
- data/lib/shamu/auditing/support.rb +14 -2
- data/lib/shamu/entities/active_record.rb +16 -2
- data/lib/shamu/entities/active_record_soft_destroy.rb +7 -3
- data/lib/shamu/entities/entity.rb +1 -1
- data/lib/shamu/entities/entity_lookup_service.rb +137 -0
- data/lib/shamu/entities/entity_path.rb +6 -9
- data/lib/shamu/entities/list.rb +8 -2
- data/lib/shamu/entities/list_scope/paging.rb +3 -3
- data/lib/shamu/entities/list_scope/sorting.rb +21 -2
- data/lib/shamu/entities/list_scope/window_paging.rb +96 -0
- data/lib/shamu/entities/list_scope.rb +2 -2
- data/lib/shamu/entities/opaque_entity_lookup_service.rb +59 -0
- data/lib/shamu/entities/opaque_id.rb +54 -0
- data/lib/shamu/entities/paged_list.rb +137 -0
- data/lib/shamu/entities.rb +5 -1
- data/lib/shamu/events/active_record/service.rb +1 -2
- data/lib/shamu/events/in_memory/service.rb +1 -2
- data/lib/shamu/features/conditions/percentage.rb +3 -3
- data/lib/shamu/features/features_service.rb +2 -2
- data/lib/shamu/features/toggle.rb +2 -3
- data/lib/shamu/json_api/context.rb +0 -1
- data/lib/shamu/json_api/rails/controller.rb +0 -2
- data/lib/shamu/rails/controller.rb +0 -1
- data/lib/shamu/rails/entity.rb +1 -1
- data/lib/shamu/security/policy.rb +1 -2
- data/lib/shamu/services/active_record.rb +16 -0
- data/lib/shamu/services/active_record_crud.rb +32 -22
- data/lib/shamu/services/lazy_transform.rb +31 -0
- data/lib/shamu/services/request_support.rb +3 -2
- data/lib/shamu/services/service.rb +11 -3
- data/lib/shamu/to_model_id_extension.rb +2 -1
- data/lib/shamu/version.rb +2 -1
- data/shamu.gemspec +2 -1
- data/spec/lib/shamu/active_record_support.rb +6 -0
- data/spec/lib/shamu/attributes/assignment_spec.rb +69 -5
- data/spec/lib/shamu/attributes/camel_case_spec.rb +33 -0
- data/spec/lib/shamu/attributes/validation_spec.rb +9 -1
- data/spec/lib/shamu/attributes_spec.rb +4 -0
- data/spec/lib/shamu/entities/active_record_spec.rb +27 -0
- data/spec/lib/shamu/entities/entity_lookup_models.rb +11 -0
- data/spec/lib/shamu/entities/entity_lookup_service_spec.rb +77 -0
- data/spec/lib/shamu/entities/entity_path_spec.rb +3 -4
- data/spec/lib/shamu/entities/list_scope/paging_spec.rb +7 -3
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +1 -7
- data/spec/lib/shamu/entities/opaque_entity_lookup_service_spec.rb +39 -0
- data/spec/lib/shamu/entities/opaque_id_spec.rb +30 -0
- data/spec/lib/shamu/entities/paged_list_spec.rb +170 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +10 -1
- data/spec/lib/shamu/services/lazy_transform_spec.rb +14 -0
- data/spec/lib/shamu/to_model_id_extension_spec.rb +5 -1
- data/spec/support/active_record.rb +1 -1
- metadata +24 -4
@@ -0,0 +1,137 @@
|
|
1
|
+
require "shamu/services"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Entities
|
5
|
+
|
6
|
+
# Looks up entities from compiled {EntityPath} strings allowing references
|
7
|
+
# to be stored as opaque values in an external service and later looked up
|
8
|
+
# without knowing which services are required to look up the entities.
|
9
|
+
#
|
10
|
+
# Useful for implementing a `node` field in a Relay GraphQL endpoint or
|
11
|
+
# resolving entities in an {Auditing::AuditingService audit log}.
|
12
|
+
#
|
13
|
+
# {#lookup} maps entity types to the service class used to look up entities
|
14
|
+
# of that type. The services must implement the well known `#lookup` method
|
15
|
+
# that takes an array of ids as an argument.
|
16
|
+
#
|
17
|
+
# If there is no entry mapping in the `entity_map` provided to the
|
18
|
+
# constructor then EntityLookupService will attempt to locate a service
|
19
|
+
# named {Entities}Service in the same namespace as the entity type.
|
20
|
+
#
|
21
|
+
# The EntityLookupService should be configured as a per-request singleton
|
22
|
+
# in Scorpion with all custom entity mappings set.
|
23
|
+
#
|
24
|
+
# ```
|
25
|
+
# Scorpion.prepare do
|
26
|
+
# capture Shamu::Entities::EntityLookupService do |scorpion|
|
27
|
+
# scorpion.new( Shamu::Entities::EntityLookupService, { "User" => Users::ExternalUsersService }, {} )
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# ```
|
31
|
+
class EntityLookupService < Shamu::Services::Service
|
32
|
+
|
33
|
+
def initialize( entity_map = nil )
|
34
|
+
entity_map ||= {}
|
35
|
+
@entity_map_cache = Hash.new do |hash, entity_type|
|
36
|
+
hash[ entity_type ] = entity_map[ entity_type ] \
|
37
|
+
|| entity_map[ entity_type.to_s ] \
|
38
|
+
|| find_implicit_service_class( entity_type.to_s )
|
39
|
+
end
|
40
|
+
super()
|
41
|
+
end
|
42
|
+
|
43
|
+
# Gets the class of the service used to look up entities of the given
|
44
|
+
# type. Use a scorpion to get an instance of the service class.
|
45
|
+
#
|
46
|
+
# @return [Shamu::Services::Service] a service that implements `#lookup`.
|
47
|
+
def service_class_for( entity_type )
|
48
|
+
entity_map_cache[ entity_type.to_sym ]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Map the given entities to their {EntityPath} that can later be used to
|
52
|
+
# {#lookup} the given entity.
|
53
|
+
def ids( entities )
|
54
|
+
Array.wrap( entities ).map do |entity|
|
55
|
+
EntityPath.compose_entity_path( [ entity ] )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Map the encoded ids back to their raw record IDs discarding any type
|
60
|
+
# information.
|
61
|
+
#
|
62
|
+
# @param [Array<String>] ids an array of ids encoded with {#ids}.
|
63
|
+
def record_ids( ids )
|
64
|
+
Array.wrap( ids ).map do |id|
|
65
|
+
EntityPath.decompose_entity_path( id ).first.last.to_model_id
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Look up all the entities from their composed {EntityPath}.
|
70
|
+
#
|
71
|
+
# @param [Array<String>] ids an array of {EntityPath} strings.
|
72
|
+
# @return [EntityList<Entity>] the entities in the same order as the
|
73
|
+
# given ids.
|
74
|
+
def lookup( *ids ) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
75
|
+
types = {}
|
76
|
+
|
77
|
+
# Decompose entity paths and group by entity type
|
78
|
+
ids.each do |composed|
|
79
|
+
path = EntityPath.decompose_entity_path( composed )
|
80
|
+
fail "Only root entities can be restored" unless path.size == 1
|
81
|
+
type, id = path.first
|
82
|
+
|
83
|
+
types[ type ] ||= { paths: [], ids: [] }
|
84
|
+
|
85
|
+
types[ type ][ :paths ] << composed
|
86
|
+
types[ type ][ :ids ] << id
|
87
|
+
end
|
88
|
+
|
89
|
+
# Short-circuit if we only have one entity type
|
90
|
+
if types.size == 1
|
91
|
+
service_class = service_class_for( types.first.first )
|
92
|
+
service = scorpion.fetch service_class
|
93
|
+
|
94
|
+
return service.lookup( *types.first.last[ :ids ] )
|
95
|
+
end
|
96
|
+
|
97
|
+
# Lookup all entities in batches
|
98
|
+
hydrated = types.map do |type, map|
|
99
|
+
service_class = service_class_for( type )
|
100
|
+
service = scorpion.fetch service_class
|
101
|
+
|
102
|
+
entities = service.lookup( *map[ :ids ] )
|
103
|
+
|
104
|
+
Hash[ map[ :paths ].zip( entities ) ]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Map found entities back to their original input order
|
108
|
+
mapped = ids.map do |id|
|
109
|
+
found = nil
|
110
|
+
hydrated.each do |map|
|
111
|
+
break if found = map[ id ]
|
112
|
+
end
|
113
|
+
|
114
|
+
found
|
115
|
+
end
|
116
|
+
|
117
|
+
Entities::List.new mapped
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
attr_reader :entity_map_cache
|
123
|
+
|
124
|
+
def find_implicit_service_class( entity_type )
|
125
|
+
namespace = entity_type.deconstantize
|
126
|
+
type = entity_type.demodulize
|
127
|
+
|
128
|
+
service_name = [
|
129
|
+
namespace,
|
130
|
+
"#{ type.pluralize }Service"
|
131
|
+
].join( "::" )
|
132
|
+
|
133
|
+
service_name.constantize
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -1,14 +1,13 @@
|
|
1
1
|
module Shamu
|
2
2
|
module Entities
|
3
3
|
|
4
|
-
# An entity path
|
4
|
+
# An entity path describes one or more levels of parent/child relationships
|
5
5
|
# that can be used to navigate from the root entity to a target entity.
|
6
6
|
#
|
7
7
|
# Entity paths can be used to identify polymorphic relationships between
|
8
8
|
# entities managed by difference services.
|
9
9
|
module EntityPath
|
10
|
-
|
11
|
-
|
10
|
+
extend self # rubocop:disable Style/ModuleFunction
|
12
11
|
|
13
12
|
# Composes an array of entities describing the path from the root entity
|
14
13
|
# to the leaf into a string.
|
@@ -29,13 +28,11 @@ module Shamu
|
|
29
28
|
def compose_entity_path( entities )
|
30
29
|
return unless entities.present?
|
31
30
|
|
32
|
-
entities.
|
33
|
-
|
34
|
-
|
35
|
-
end
|
31
|
+
entities.map do |entity|
|
32
|
+
compose_single_entity( entity )
|
33
|
+
end.join( "/" )
|
36
34
|
end
|
37
35
|
|
38
|
-
|
39
36
|
# Decompose an entity path into an array of arrays of entity classes with
|
40
37
|
# their ids.
|
41
38
|
#
|
@@ -84,4 +81,4 @@ module Shamu
|
|
84
81
|
|
85
82
|
end
|
86
83
|
end
|
87
|
-
end
|
84
|
+
end
|
data/lib/shamu/entities/list.rb
CHANGED
@@ -16,11 +16,17 @@ module Shamu
|
|
16
16
|
entities.each( &block )
|
17
17
|
end
|
18
18
|
|
19
|
-
delegate :first, :count, :empty?, to: :raw_entities
|
19
|
+
delegate :first, :last, :count, :empty?, :index, to: :raw_entities
|
20
20
|
|
21
21
|
alias_method :size, :count
|
22
22
|
alias_method :length, :count
|
23
23
|
|
24
|
+
# @return [Boolean] true if the list represents a slice of a larger set.
|
25
|
+
# See {PagedList} for paged implementation.
|
26
|
+
def paged?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
24
30
|
# Get an entity by it's primary key.
|
25
31
|
# @param [Object] key the primary key to look for.
|
26
32
|
# @param [Symbol] field to use as the primary key. Default :id.
|
@@ -51,4 +57,4 @@ module Shamu
|
|
51
57
|
end
|
52
58
|
end
|
53
59
|
end
|
54
|
-
end
|
60
|
+
end
|
@@ -37,15 +37,15 @@ module Shamu
|
|
37
37
|
|
38
38
|
base.attribute :page, coerce: :to_i
|
39
39
|
base.attribute :per_page, coerce: :to_i, default: ->() { default_per_page }
|
40
|
-
base.attribute :default_per_page, coerce: :to_i,
|
40
|
+
base.attribute :default_per_page, coerce: :to_i, serialize: false
|
41
41
|
end
|
42
42
|
|
43
43
|
# @return [Boolean] true if the scope is paged.
|
44
44
|
def paged?
|
45
|
-
!!page
|
45
|
+
!!page || !!per_page
|
46
46
|
end
|
47
47
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
51
|
-
end
|
51
|
+
end
|
@@ -42,7 +42,7 @@ module Shamu
|
|
42
42
|
def self.included( base )
|
43
43
|
super
|
44
44
|
|
45
|
-
base.attribute :sort_by, coerce: ->( *values ) { parse_sort_by( values ) }
|
45
|
+
base.attribute :sort_by, as: :order, coerce: ->( *values ) { parse_sort_by( values ) }
|
46
46
|
end
|
47
47
|
|
48
48
|
# @return [Boolean] true if the scope is paged.
|
@@ -50,8 +50,27 @@ module Shamu
|
|
50
50
|
!!sort_by
|
51
51
|
end
|
52
52
|
|
53
|
+
# @return [Hash] gets a normalized hash of attribute to direction with
|
54
|
+
# all transforms applied.
|
55
|
+
def sort_by_resolved
|
56
|
+
return sort_by unless reverse_sort?
|
57
|
+
|
58
|
+
sort_by.each_with_object( {} ) do |( attribute, direction ), resolved|
|
59
|
+
resolved[ attribute ] = direction == :asc ? :desc : :asc
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
53
63
|
private
|
54
64
|
|
65
|
+
def reverse_sort?
|
66
|
+
@reverse_sort
|
67
|
+
end
|
68
|
+
|
69
|
+
def reverse_sort!
|
70
|
+
@reverse_sort = true
|
71
|
+
self.sort_by = { id: :asc } unless sort_by_set?
|
72
|
+
end
|
73
|
+
|
55
74
|
def parse_sort_by( arguments )
|
56
75
|
Array( arguments ).each_with_object( {} ) do |arg, sorted|
|
57
76
|
case arg
|
@@ -73,4 +92,4 @@ module Shamu
|
|
73
92
|
end
|
74
93
|
end
|
75
94
|
end
|
76
|
-
end
|
95
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Entities
|
3
|
+
class ListScope
|
4
|
+
|
5
|
+
# Limit/offset style paging using first/after naming conventions typical
|
6
|
+
# in GraphQL implementations.
|
7
|
+
#
|
8
|
+
# ```
|
9
|
+
# class UsersListScope < Shamu::Entities::ListScope
|
10
|
+
# include Shamu::Entities::ListScope::WindowPaging
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# scope = UsersListScope.coerce!( params )
|
14
|
+
# scope.first # => 25
|
15
|
+
# scope.after # => 75
|
16
|
+
# ```
|
17
|
+
module WindowPaging
|
18
|
+
|
19
|
+
# ============================================================================
|
20
|
+
# @!group Attributes
|
21
|
+
#
|
22
|
+
|
23
|
+
# @!attribute first
|
24
|
+
# @return [Integer] get the first n records.
|
25
|
+
|
26
|
+
# @!attribute after
|
27
|
+
# @return [Integer] the number of records to skip from the beginning.
|
28
|
+
|
29
|
+
# @!attribute last
|
30
|
+
# @return [Integer] get the last n records.
|
31
|
+
|
32
|
+
# @!attribute before
|
33
|
+
# @return [Integer] the number of records to skip from the end.
|
34
|
+
|
35
|
+
#
|
36
|
+
# @!endgroup Attributes
|
37
|
+
|
38
|
+
def self.included( base ) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
39
|
+
super
|
40
|
+
|
41
|
+
base.attribute :first, coerce: :to_i, default: ->() { default_first }
|
42
|
+
base.attribute :default_first, coerce: :to_i, serialize: false
|
43
|
+
|
44
|
+
base.attribute :after, coerce: :to_i
|
45
|
+
|
46
|
+
base.attribute :last, default: ->() { default_last }, coerce: ->( value ) do
|
47
|
+
ensure_includes_sorting!
|
48
|
+
reverse_sort!
|
49
|
+
|
50
|
+
value.to_i if value
|
51
|
+
end
|
52
|
+
|
53
|
+
base.attribute :default_last, serialize: false, coerce: ->( value ) do
|
54
|
+
ensure_includes_sorting!
|
55
|
+
reverse_sort!
|
56
|
+
|
57
|
+
value.to_i if value
|
58
|
+
end
|
59
|
+
|
60
|
+
base.attribute :before, coerce: ->( value ) do
|
61
|
+
ensure_includes_sorting!
|
62
|
+
reverse_sort!
|
63
|
+
|
64
|
+
value.to_i if value
|
65
|
+
end
|
66
|
+
|
67
|
+
base.validate :only_first_or_last
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean] true if the scope is paged.
|
71
|
+
def window_paged?
|
72
|
+
first? || last?
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def first?
|
78
|
+
!!first || !!after
|
79
|
+
end
|
80
|
+
|
81
|
+
def last?
|
82
|
+
!!last || !!before
|
83
|
+
end
|
84
|
+
|
85
|
+
def only_first_or_last
|
86
|
+
errors.add :base, :only_first_or_last if first? && last?
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_includes_sorting!
|
90
|
+
raise "Must include Shamu::Entities::ListScope::Sorting to use last/before" unless respond_to?( :reverse_sort!, true ) # rubocop:disable Metrics/LineLength
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -24,11 +24,11 @@ module Shamu
|
|
24
24
|
class ListScope
|
25
25
|
include Attributes
|
26
26
|
include Attributes::Assignment
|
27
|
-
include Attributes::FluidAssignment
|
28
27
|
include Attributes::Validation
|
29
28
|
|
30
29
|
require "shamu/entities/list_scope/paging"
|
31
30
|
require "shamu/entities/list_scope/scoped_paging"
|
31
|
+
require "shamu/entities/list_scope/window_paging"
|
32
32
|
require "shamu/entities/list_scope/dates"
|
33
33
|
require "shamu/entities/list_scope/sorting"
|
34
34
|
|
@@ -102,4 +102,4 @@ module Shamu
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
end
|
105
|
-
end
|
105
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "shamu/services"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
module Entities
|
5
|
+
|
6
|
+
# Implements an {EntityLookupService} that works with {OpaqueId} encoded
|
7
|
+
# values to obfuscate the contents and type of record identified by the id.
|
8
|
+
# Useful for implementing guidelines for globally unique IDs in a GraphQL
|
9
|
+
# system.
|
10
|
+
#
|
11
|
+
# ```
|
12
|
+
# Scorpion.prepare do
|
13
|
+
# capture Shamu::Entities::EntityLookupService do |scorpion|
|
14
|
+
# scorpion.new( Shamu::Entities::OpaqueEntityLookupService, { "User" => Users::ExternalUsersService }, {} )
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# ```
|
18
|
+
class OpaqueEntityLookupService < EntityLookupService
|
19
|
+
|
20
|
+
# ============================================================================
|
21
|
+
# @!group Dependencies
|
22
|
+
#
|
23
|
+
|
24
|
+
# @!attribute
|
25
|
+
# @return [EntityLookupService] the underlying lookup service to use.
|
26
|
+
attr_dependency :lookup_service, EntityLookupService do |scorpion|
|
27
|
+
scorpion.new( EntityLookupService, { entity_map: entity_map }, {} )
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
#
|
32
|
+
# @!endgroup Dependencies
|
33
|
+
|
34
|
+
# (see {EntityLookupService#ids)
|
35
|
+
def ids( entities )
|
36
|
+
super.map do |id|
|
37
|
+
OpaqueId.opaque_id( id )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# (see {EntityLookupService#record_ids)
|
42
|
+
def record_ids( ids )
|
43
|
+
super( ids_to_entity_paths( ids ) )
|
44
|
+
end
|
45
|
+
|
46
|
+
# (see {EntityLookupService#lookup)
|
47
|
+
def lookup( *ids )
|
48
|
+
super( *ids_to_entity_paths( ids ) )
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def ids_to_entity_paths( ids )
|
54
|
+
Array.wrap( ids ).map { |id| OpaqueId.to_entity_path( id ) }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Entities
|
3
|
+
|
4
|
+
# A collection of helper methods for building and reconstructing opaque
|
5
|
+
# entity ids. These ids can be used to uniquely reference a resource within
|
6
|
+
# the system without knowing the type or service.
|
7
|
+
#
|
8
|
+
# See {EntityLookupService} for lookup up resources by opaque ID.
|
9
|
+
module OpaqueId
|
10
|
+
module_function
|
11
|
+
|
12
|
+
PREFIX = "::".freeze
|
13
|
+
PATTERN = %r{\A#{ PREFIX }[a-zA-Z0-9+/]+={0,3}\z}
|
14
|
+
|
15
|
+
# @return [String] an opaque value that uniquely identifies the
|
16
|
+
# entity.
|
17
|
+
def opaque_id( entity )
|
18
|
+
path = if entity.is_a?( String )
|
19
|
+
entity
|
20
|
+
else
|
21
|
+
Entity.compose_entity_path( [ entity ] )
|
22
|
+
end
|
23
|
+
|
24
|
+
"#{ PREFIX }#{ Base64.strict_encode64( path ) }"
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String,Integer] the encoded raw record id.
|
28
|
+
def to_model_id( opaque_id )
|
29
|
+
if path = to_entity_path( opaque_id )
|
30
|
+
path = EntityPath.decompose_entity_path( path )
|
31
|
+
path.first.last.to_model_id
|
32
|
+
else
|
33
|
+
opaque_id.to_model_id
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Array<[String, String]>] decodes the id to it's {EntityPath}.
|
38
|
+
def to_entity_path( opaque_id )
|
39
|
+
return nil unless opaque_id && opaque_id.start_with?( PREFIX )
|
40
|
+
|
41
|
+
id = opaque_id[ PREFIX.length..-1 ]
|
42
|
+
id = Base64.strict_decode64( id )
|
43
|
+
id
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [String] value candidate value
|
47
|
+
# @return [Boolean] true if the given value is an opaque id.
|
48
|
+
def opaque_id?( value )
|
49
|
+
return unless value
|
50
|
+
PATTERN =~ value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Entities
|
3
|
+
|
4
|
+
# A list of {Entities::Entity} records.
|
5
|
+
class PagedList < List
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# @param [Enumerable] entities the raw list of entities.
|
9
|
+
# @param [Integer, #call] total_count the number (or a proc that resolves
|
10
|
+
# to a number) of records in the entire set.
|
11
|
+
# @param [Integer, #call] limit the maximum number (or a proc that resolves to a
|
12
|
+
# number) of records in the page represented by the list.
|
13
|
+
# @param [Integer, #call] offset the number (or a proc that resolves to a
|
14
|
+
# number) offset from the start of the set that this list represents.
|
15
|
+
# @param [Boolean,#call] has_next true if there is another page
|
16
|
+
# available or a proc that returns a bool.
|
17
|
+
# @param [Boolean,#call] has_previous true if there is a previous page
|
18
|
+
# available or a proc that returns a bool.
|
19
|
+
def initialize( entities,
|
20
|
+
total_count: :not_set,
|
21
|
+
limit: :not_set,
|
22
|
+
offset: :not_set,
|
23
|
+
has_next: :not_set,
|
24
|
+
has_previous: :not_set )
|
25
|
+
super( entities )
|
26
|
+
|
27
|
+
@total_count = total_count
|
28
|
+
@limit = limit
|
29
|
+
@offset = offset
|
30
|
+
@has_next = has_next
|
31
|
+
@has_previous = has_previous
|
32
|
+
end
|
33
|
+
|
34
|
+
# (see List#paged?)
|
35
|
+
def paged?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Integer] the total number of records in the set.
|
40
|
+
def total_count
|
41
|
+
if @total_count == :not_set
|
42
|
+
raw_entities.total_count
|
43
|
+
elsif @total_count.respond_to?( :call )
|
44
|
+
@total_count = @total_count.call
|
45
|
+
else
|
46
|
+
@total_count
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Integer] the maximum number of records to return in each page.
|
51
|
+
def limit
|
52
|
+
if @limit == :not_set
|
53
|
+
if raw_entities.respond_to?( :limit_value )
|
54
|
+
raw_entities.limit_value
|
55
|
+
elsif raw_entities.respond_to?( :limit )
|
56
|
+
raw_entities.limit
|
57
|
+
end
|
58
|
+
elsif @limit.respond_to?( :call )
|
59
|
+
@limit = @limit.call
|
60
|
+
else
|
61
|
+
@limit
|
62
|
+
end
|
63
|
+
end
|
64
|
+
alias_method :per_page, :limit
|
65
|
+
|
66
|
+
# @return [Integer] the absolute offset into the set for the window of
|
67
|
+
# data that that this list contains.
|
68
|
+
def offset
|
69
|
+
if @offset == :not_set
|
70
|
+
if raw_entities.respond_to?( :offset_value )
|
71
|
+
raw_entities.offset_value
|
72
|
+
elsif raw_entities.respond_to?( :offset )
|
73
|
+
raw_entities.offset
|
74
|
+
end
|
75
|
+
elsif @offset.respond_to?( :call )
|
76
|
+
@offset = @offset.call
|
77
|
+
else
|
78
|
+
@offset
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Integer] the current page number.
|
83
|
+
def current_page
|
84
|
+
if limit > 0
|
85
|
+
( offset / limit ).to_i + 1
|
86
|
+
else
|
87
|
+
1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Boolean] true if there is another page of data available.
|
92
|
+
def next?
|
93
|
+
if @has_next == :not_set
|
94
|
+
if raw_entities.respond_to?( :has_next? )
|
95
|
+
raw_entities.has_next?
|
96
|
+
elsif raw_entities.respond_to?( :last_page? )
|
97
|
+
!raw_entities.last_page?
|
98
|
+
end
|
99
|
+
elsif @has_next.respond_to?( :call )
|
100
|
+
@has_next = @has_next.call
|
101
|
+
else
|
102
|
+
@has_next
|
103
|
+
end
|
104
|
+
end
|
105
|
+
alias_method :has_next?, :next?
|
106
|
+
|
107
|
+
# @return [Boolean] true if this list represents the last page in the
|
108
|
+
# set.
|
109
|
+
def last?
|
110
|
+
!next?
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [Boolean] true if there is another page of data available.
|
114
|
+
def previous?
|
115
|
+
if @has_previous == :not_set
|
116
|
+
if raw_entities.respond_to?( :has_previous? )
|
117
|
+
raw_entities.has_previous?
|
118
|
+
elsif raw_entities.respond_to?( :first_page? )
|
119
|
+
!raw_entities.first_page?
|
120
|
+
end
|
121
|
+
elsif @has_previous.respond_to?( :call )
|
122
|
+
@has_previous = @has_previous.call
|
123
|
+
else
|
124
|
+
@has_previous
|
125
|
+
end
|
126
|
+
end
|
127
|
+
alias_method :has_prev?, :previous?
|
128
|
+
alias_method :has_previous, :previous?
|
129
|
+
|
130
|
+
# @return [Boolean] true if this list represents the first page in the
|
131
|
+
# set.
|
132
|
+
def first?
|
133
|
+
!previous?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/lib/shamu/entities.rb
CHANGED
@@ -4,9 +4,13 @@ module Shamu
|
|
4
4
|
require "shamu/entities/entity"
|
5
5
|
require "shamu/entities/null_entity"
|
6
6
|
require "shamu/entities/list"
|
7
|
+
require "shamu/entities/paged_list"
|
7
8
|
require "shamu/entities/list_scope"
|
8
9
|
require "shamu/entities/identity_cache"
|
9
10
|
require "shamu/entities/entity_path"
|
10
11
|
require "shamu/entities/html_sanitation"
|
12
|
+
require "shamu/entities/entity_lookup_service"
|
13
|
+
require "shamu/entities/opaque_id"
|
14
|
+
require "shamu/entities/opaque_entity_lookup_service"
|
11
15
|
end
|
12
|
-
end
|
16
|
+
end
|
@@ -25,9 +25,9 @@ module Shamu
|
|
25
25
|
|
26
26
|
def user_id_hash( user_id )
|
27
27
|
if user_id.is_a?( Numeric )
|
28
|
-
|
28
|
+
user_id
|
29
29
|
else
|
30
|
-
|
30
|
+
user_id.sub( "-", "" ).to_i( 16 )
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -41,4 +41,4 @@ module Shamu
|
|
41
41
|
|
42
42
|
end
|
43
43
|
end
|
44
|
-
end
|
44
|
+
end
|