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.
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