shamu 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +55 -20
  3. data/Gemfile +3 -3
  4. data/Gemfile.lock +13 -11
  5. data/circle.yml +1 -1
  6. data/lib/shamu/attributes/assignment.rb +44 -5
  7. data/lib/shamu/attributes/camel_case.rb +21 -0
  8. data/lib/shamu/attributes/validation.rb +13 -1
  9. data/lib/shamu/attributes/validators/valid_validator.rb +14 -0
  10. data/lib/shamu/attributes/validators.rb +7 -0
  11. data/lib/shamu/attributes.rb +13 -8
  12. data/lib/shamu/auditing/auditing_service.rb +1 -5
  13. data/lib/shamu/auditing/support.rb +14 -2
  14. data/lib/shamu/entities/active_record.rb +16 -2
  15. data/lib/shamu/entities/active_record_soft_destroy.rb +7 -3
  16. data/lib/shamu/entities/entity.rb +1 -1
  17. data/lib/shamu/entities/entity_lookup_service.rb +137 -0
  18. data/lib/shamu/entities/entity_path.rb +6 -9
  19. data/lib/shamu/entities/list.rb +8 -2
  20. data/lib/shamu/entities/list_scope/paging.rb +3 -3
  21. data/lib/shamu/entities/list_scope/sorting.rb +21 -2
  22. data/lib/shamu/entities/list_scope/window_paging.rb +96 -0
  23. data/lib/shamu/entities/list_scope.rb +2 -2
  24. data/lib/shamu/entities/opaque_entity_lookup_service.rb +59 -0
  25. data/lib/shamu/entities/opaque_id.rb +54 -0
  26. data/lib/shamu/entities/paged_list.rb +137 -0
  27. data/lib/shamu/entities.rb +5 -1
  28. data/lib/shamu/events/active_record/service.rb +1 -2
  29. data/lib/shamu/events/in_memory/service.rb +1 -2
  30. data/lib/shamu/features/conditions/percentage.rb +3 -3
  31. data/lib/shamu/features/features_service.rb +2 -2
  32. data/lib/shamu/features/toggle.rb +2 -3
  33. data/lib/shamu/json_api/context.rb +0 -1
  34. data/lib/shamu/json_api/rails/controller.rb +0 -2
  35. data/lib/shamu/rails/controller.rb +0 -1
  36. data/lib/shamu/rails/entity.rb +1 -1
  37. data/lib/shamu/security/policy.rb +1 -2
  38. data/lib/shamu/services/active_record.rb +16 -0
  39. data/lib/shamu/services/active_record_crud.rb +32 -22
  40. data/lib/shamu/services/lazy_transform.rb +31 -0
  41. data/lib/shamu/services/request_support.rb +3 -2
  42. data/lib/shamu/services/service.rb +11 -3
  43. data/lib/shamu/to_model_id_extension.rb +2 -1
  44. data/lib/shamu/version.rb +2 -1
  45. data/shamu.gemspec +2 -1
  46. data/spec/lib/shamu/active_record_support.rb +6 -0
  47. data/spec/lib/shamu/attributes/assignment_spec.rb +69 -5
  48. data/spec/lib/shamu/attributes/camel_case_spec.rb +33 -0
  49. data/spec/lib/shamu/attributes/validation_spec.rb +9 -1
  50. data/spec/lib/shamu/attributes_spec.rb +4 -0
  51. data/spec/lib/shamu/entities/active_record_spec.rb +27 -0
  52. data/spec/lib/shamu/entities/entity_lookup_models.rb +11 -0
  53. data/spec/lib/shamu/entities/entity_lookup_service_spec.rb +77 -0
  54. data/spec/lib/shamu/entities/entity_path_spec.rb +3 -4
  55. data/spec/lib/shamu/entities/list_scope/paging_spec.rb +7 -3
  56. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +1 -7
  57. data/spec/lib/shamu/entities/opaque_entity_lookup_service_spec.rb +39 -0
  58. data/spec/lib/shamu/entities/opaque_id_spec.rb +30 -0
  59. data/spec/lib/shamu/entities/paged_list_spec.rb +170 -0
  60. data/spec/lib/shamu/services/active_record_crud_spec.rb +10 -1
  61. data/spec/lib/shamu/services/lazy_transform_spec.rb +14 -0
  62. data/spec/lib/shamu/to_model_id_extension_spec.rb +5 -1
  63. data/spec/support/active_record.rb +1 -1
  64. 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 descrives one or more levels of parent/child relationships
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
- module_function
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.each_with_object( "" ) do |entity, path|
33
- path << "/" if path.present?
34
- path << compose_single_entity( entity )
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
@@ -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, default: 25, serialize: false
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
@@ -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
@@ -110,7 +110,6 @@ module Shamu
110
110
  end
111
111
 
112
112
  dispatch_messages( state, runner_id, limit )
113
-
114
113
  ensure
115
114
  mutex.synchronize do
116
115
  state[ :dispatching ] = false
@@ -171,4 +170,4 @@ module Shamu
171
170
  end
172
171
  end
173
172
  end
174
- end
173
+ end
@@ -75,7 +75,6 @@ module Shamu
75
75
  end
76
76
 
77
77
  dispatch_messages( state )
78
-
79
78
  ensure
80
79
  mutex.synchronize do
81
80
  state[ :dispatching ] = false
@@ -94,4 +93,4 @@ module Shamu
94
93
  end
95
94
  end
96
95
  end
97
- end
96
+ 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
- return user_id
28
+ user_id
29
29
  else
30
- return user_id.sub( "-", "" ).to_i( 16 )
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