shamu 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|