pragma-decorator 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,21 +2,58 @@
2
2
 
3
3
  module Pragma
4
4
  module Decorator
5
+ # Associations provide a way to define related records on your decorators.
6
+ #
7
+ # Once you define an association, it can be expanded by API clients through the +expand+ query
8
+ # parameter to load the full record.
9
+ #
10
+ # @example Defining and expanding an association
11
+ # class ArticleDecorator < Pragma::Decorator::Base
12
+ # belongs_to :author, decorator: API::V1::User::Decorator
13
+ # end
14
+ #
15
+ # # This will return a hash whose `author` key is the ID of the article's author.
16
+ # ArticleDecorator.new(article).to_hash
17
+ #
18
+ # # This will return a hash whose `author` key is a hash representing the decorated
19
+ # # article's author.
20
+ # ArticleDecorator.new(article).to_hash(user_options: { expand: ['author'] })
5
21
  module Association
6
22
  def self.included(klass)
7
23
  klass.extend ClassMethods
8
24
  klass.include InstanceMethods
9
25
  end
10
26
 
11
- module ClassMethods # rubocop:disable Style/Documentation
27
+ module ClassMethods # :nodoc:
28
+ # Returns the associations defined on this decorator.
29
+ #
30
+ # @return [Hash{Symbol => Reflection}] the associations
12
31
  def associations
13
32
  @associations ||= {}
14
33
  end
15
34
 
35
+ # Defines a +belongs_to+ association on this decorator.
36
+ #
37
+ # This will first create an association definition and then define a new property with the
38
+ # name of the association.
39
+ #
40
+ # This method supports all the usual options accepted by +#property+.
41
+ #
42
+ # @param property_name [Symbol] name of the association
43
+ # @param options [Hash] the association's options
16
44
  def belongs_to(property_name, options = {})
17
45
  define_association :belongs_to, property_name, options
18
46
  end
19
47
 
48
+ # Defines a +has_one+ association on this decorator.
49
+ #
50
+ # This will first create an association definition and then define a new property with the
51
+ # name of the association.
52
+ #
53
+ # This method supports all the usual options accepted by +#property+.
54
+ #
55
+ # @param property_name [Symbol] name of the association
56
+ # @param options [Hash] the association's options
20
57
  def has_one(property_name, options = {}) # rubocop:disable Naming/PredicateName
21
58
  define_association :has_one, property_name, options
22
59
  end
@@ -35,12 +72,12 @@ module Pragma
35
72
  def create_association_property(_type, property_name, options)
36
73
  property_options = options.dup.tap { |po| po.delete(:decorator) }.merge(
37
74
  exec_context: :decorator,
38
- as: property_name,
75
+ as: options[:as] || property_name,
39
76
  getter: (lambda do |decorator:, user_options:, **_args|
40
- Binding.new(
77
+ Bond.new(
41
78
  reflection: decorator.class.associations[property_name],
42
79
  decorator: decorator
43
- ).render(user_options[:expand])
80
+ ).render(user_options)
44
81
  end)
45
82
  )
46
83
 
@@ -48,7 +85,7 @@ module Pragma
48
85
  end
49
86
  end
50
87
 
51
- module InstanceMethods
88
+ module InstanceMethods # :nodoc:
52
89
  def validate_expansion(expand)
53
90
  check_parent_associations_are_expanded(expand)
54
91
  check_expanded_associations_exist(expand)
@@ -7,9 +7,7 @@ module Pragma
7
7
  module Decorator
8
8
  # This is the base decorator that all your resource-specific decorators should extend from.
9
9
  #
10
- # It is already configured to render your resources in JSON.
11
- #
12
- # @author Alessandro Desantis
10
+ # It is already configured to render your resources as JSON.
13
11
  class Base < Roar::Decorator
14
12
  feature Roar::JSON
15
13
 
@@ -2,6 +2,33 @@
2
2
 
3
3
  module Pragma
4
4
  module Decorator
5
+ # This module is used to represent collections of objects.
6
+ #
7
+ # It will wrap the collection in a +data+ property so that you can include meta-data about the
8
+ # collection at the root level.
9
+ #
10
+ # @example Using Collection to include a total count
11
+ # class ArticlesDecorator < Pragma::Decorator::Base
12
+ # include Pragma::Decorator::Collection
13
+ #
14
+ # decorate_with ArticleDecorator
15
+ #
16
+ # property :total_count, exec_context: :decorator
17
+ #
18
+ # def total_count
19
+ # represented.count
20
+ # end
21
+ # end
22
+ #
23
+ # # {
24
+ # # "data": [
25
+ # # { "...": "..." },
26
+ # # { "...": "..." },
27
+ # # { "...": "..." }
28
+ # # ],
29
+ # # "total_count": 150
30
+ # # }
31
+ # ArticlesDecorator.new(Article.all).to_hash
5
32
  module Collection
6
33
  def self.included(klass)
7
34
  klass.include InstanceMethods
@@ -12,13 +39,21 @@ module Pragma
12
39
  end
13
40
  end
14
41
 
15
- module InstanceMethods
42
+ module InstanceMethods # :nodoc:
43
+ # Overrides the type of the resource to be +list+, for compatibility with {Type}.
44
+ #
45
+ # @see Type
16
46
  def type
17
- 'collection'
47
+ 'list'
18
48
  end
19
49
  end
20
50
 
21
- module ClassMethods
51
+ module ClassMethods # :nodoc:
52
+ # Defines the decorator to use for each resource in the collection.
53
+ #
54
+ # @param decorator [Class] a decorator class
55
+ #
56
+ # @todo Accept a callable/block or document how to decorate polymorphic collections
22
57
  def decorate_with(decorator)
23
58
  collection :represented, as: :data, exec_context: :decorator, decorator: decorator
24
59
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Pagination
6
+ module Adapter
7
+ # This is the base pagination adapter.
8
+ #
9
+ # @abstract Subclass and override the abstract methods to implement an adapter
10
+ #
11
+ # @api private
12
+ class Base
13
+ # @!attribute [r] collection
14
+ # @return [Object] the collection this adapter is working with
15
+ attr_reader :collection
16
+
17
+ class << self
18
+ # Returns whether this adapter supports the given collection.
19
+ #
20
+ # @return [Boolean] whether the adapter supports the given collection
21
+ #
22
+ # @see Adapter.load_for
23
+ def supports?(_collection)
24
+ fail NotImplementedError
25
+ end
26
+ end
27
+
28
+ # Initializes the adapter.
29
+ #
30
+ # @param collection [Object] the collection to work with
31
+ def initialize(collection)
32
+ @collection = collection
33
+ end
34
+
35
+ # Returns the total number of entries in the collection.
36
+ #
37
+ # @return [Integer] the total number of entries in the collection
38
+ def total_entries
39
+ fail NotImplementedError
40
+ end
41
+
42
+ # Returns the number of entries per page in the collection.
43
+ #
44
+ # @return [Integer] the number of entries per page in the collection
45
+ def per_page
46
+ fail NotImplementedError
47
+ end
48
+
49
+ # Returns the total number of pages in the collection.
50
+ #
51
+ # @return [Integer] the total number of pages in the collection
52
+ def total_pages
53
+ fail NotImplementedError
54
+ end
55
+
56
+ # Returns the number of the previous page, if any.
57
+ #
58
+ # @return [Integer|NilClass] the number of the previous page, if any
59
+ def previous_page
60
+ fail NotImplementedError
61
+ end
62
+
63
+ # Returns the number of the current page.
64
+ #
65
+ # @return [Integer] the number of the current page
66
+ def current_page
67
+ fail NotImplementedError
68
+ end
69
+
70
+ # Returns the number of the next page, if any.
71
+ #
72
+ # @return [Integer|NilClass] the number of the next page, if any
73
+ def next_page
74
+ fail NotImplementedError
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Pagination
6
+ module Adapter
7
+ # This adapter provides support for retireving pagination information from collections
8
+ # paginated with {https://github.com/kaminari/kaminari Kaminari}.
9
+ #
10
+ # @api private
11
+ class Kaminari < Base
12
+ class << self
13
+ # Returns whether this adapter supports the given collection.
14
+ #
15
+ # Esnures that the +Kaminari+ constant is defined and that the collection responds to
16
+ # +#prev_page+.
17
+ #
18
+ # @return [Boolean] whether the adapter supports the given collection
19
+ #
20
+ # @see Adapter.load_for
21
+ def supports?(collection)
22
+ Object.const_defined?('Kaminari') && collection.respond_to?(:prev_page)
23
+ end
24
+ end
25
+
26
+ # Returns the total number of entries in the collection.
27
+ #
28
+ # @return [Integer] the total number of entries in the collection
29
+ def total_entries
30
+ collection.total_count
31
+ end
32
+
33
+ # Returns the number of entries per page in the collection.
34
+ #
35
+ # @return [Integer] the number of entries per page in the collection
36
+ def per_page
37
+ collection.limit_value
38
+ end
39
+
40
+ # Returns the total number of pages in the collection.
41
+ #
42
+ # @return [Integer] the total number of pages in the collection
43
+ def total_pages
44
+ collection.total_pages
45
+ end
46
+
47
+ # Returns the number of the previous page, if any.
48
+ #
49
+ # @return [Integer|NilClass] the number of the previous page, if any
50
+ def previous_page
51
+ collection.prev_page
52
+ end
53
+
54
+ # Returns the number of the current page.
55
+ #
56
+ # @return [Integer] the number of the current page
57
+ def current_page
58
+ collection.current_page
59
+ end
60
+
61
+ # Returns the number of the next page, if any.
62
+ #
63
+ # @return [Integer|NilClass] the number of the next page, if any
64
+ def next_page
65
+ collection.next_page
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Pagination
6
+ module Adapter
7
+ # This adapter provides support for retireving pagination information from collections
8
+ # paginated with {https://github.com/mislav/will_paginate will_paginate}.
9
+ #
10
+ # @api private
11
+ class WillPaginate < Base
12
+ class << self
13
+ # Returns whether this adapter supports the given collection.
14
+ #
15
+ # Esnures that the +WillPaginate+ constant is defined and that the collection responds
16
+ # to +#previous_page+.
17
+ #
18
+ # @return [Boolean] whether the adapter supports the given collection
19
+ #
20
+ # @see Adapter.load_for
21
+ def supports?(collection)
22
+ Object.const_defined?('WillPaginate') && collection.respond_to?(:previous_page)
23
+ end
24
+ end
25
+
26
+ # Returns the total number of entries in the collection.
27
+ #
28
+ # @return [Integer] the total number of entries in the collection
29
+ def total_entries
30
+ collection.total_entries
31
+ end
32
+
33
+ # Returns the number of entries per page in the collection.
34
+ #
35
+ # @return [Integer] the number of entries per page in the collection
36
+ def per_page
37
+ collection.per_page
38
+ end
39
+
40
+ # Returns the total number of pages in the collection.
41
+ #
42
+ # @return [Integer] the total number of pages in the collection
43
+ def total_pages
44
+ collection.total_pages
45
+ end
46
+
47
+ # Returns the number of the previous page, if any.
48
+ #
49
+ # @return [Integer|NilClass] the number of the previous page, if any
50
+ def previous_page
51
+ collection.previous_page
52
+ end
53
+
54
+ # Returns the number of the current page.
55
+ #
56
+ # @return [Integer] the number of the current page
57
+ def current_page
58
+ collection.current_page
59
+ end
60
+
61
+ # Returns the number of the next page, if any.
62
+ #
63
+ # @return [Integer|NilClass] the number of the next page, if any
64
+ def next_page
65
+ collection.next_page
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Decorator
5
+ module Pagination
6
+ # Adapters make pagination library-independent by providing support for multiple underlying
7
+ # libraries like Kaminari and will_paginate.
8
+ #
9
+ # @api private
10
+ module Adapter
11
+ # The list of supported adapters, in order of priority.
12
+ SUPPORTED_ADAPTERS = [Kaminari, WillPaginate].freeze
13
+
14
+ # Loads the adapter for the given collection.
15
+ #
16
+ # This will try {SUPPORTED_ADAPTERS} in order until it finds an adapter that supports the
17
+ # collection. When the adapter is found, it will return a new instance of it.
18
+ #
19
+ # @param collection [Object] the collection to load the adapter for
20
+ #
21
+ # @return [Adapter::Base]
22
+ #
23
+ # @see Adapter::Base.supports?
24
+ #
25
+ # @raise [AdapterError] if no adapter supports the collection
26
+ def self.load_for(collection)
27
+ adapter_klass = SUPPORTED_ADAPTERS.find do |klass|
28
+ klass.supports?(collection)
29
+ end
30
+
31
+ fail NoAdapterError unless adapter_klass
32
+
33
+ adapter_klass.new(collection)
34
+ end
35
+
36
+ # This error is raised when no adapter can be found for a collection.
37
+ class NoAdapterError < StandardError
38
+ # Initializes the adapter.
39
+ def initialize
40
+ message = <<~MESSAGE.tr("\n", ' ')
41
+ No adapter found for the collection. The available adapters are:
42
+ #{SUPPORTED_ADAPTERS.map { |a| a.to_s.split('::').last }.join(', ')}.
43
+ MESSAGE
44
+
45
+ super message
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,44 +2,93 @@
2
2
 
3
3
  module Pragma
4
4
  module Decorator
5
+ # Pagination provides support for including pagination metadata in your collection.
6
+ #
7
+ # It is particularly useful when used in conjunction with {Collection}.
8
+ #
9
+ # It supports both {https://github.com/kaminari/kaminari Kaminari} and
10
+ # {https://github.com/mislav/will_paginate will_paginate}.
11
+ #
12
+ # @example Including pagination metadata
13
+ # class ArticlesDecorator < Pragma::Decorator::Base
14
+ # include Pragma::Decorator::Collection
15
+ # include Pragma::Decorator::Pagination
16
+ # end
17
+ #
18
+ # # {
19
+ # # "data": [
20
+ # # { "...": "..." },
21
+ # # { "...": "..." },
22
+ # # { "...": "..." }
23
+ # # ],
24
+ # # "total_entries": 150,
25
+ # # "per_page": 30,
26
+ # # "total_pages": 5,
27
+ # # "previous_page": 2,
28
+ # # "current_page": 3,
29
+ # # "next_page": 4
30
+ # # }
31
+ # ArticlesDecorator.new(Article.all).to_hash
5
32
  module Pagination
6
- module InstanceMethods
33
+ module InstanceMethods # :nodoc:
34
+ # Returns the current page of the collection.
35
+ #
36
+ # @return [Integer] current page number
37
+ #
38
+ # @see Adapter::Base#current_page
7
39
  def current_page
8
- represented.current_page.to_i
40
+ adapter.current_page
9
41
  end
10
42
 
43
+ # Returns the next page of the collection.
44
+ #
45
+ # @return [Integer|NilClass] next page number, if any
46
+ #
47
+ # @see Adapter::Base#next_page
11
48
  def next_page
12
- represented.next_page
49
+ adapter.next_page
13
50
  end
14
51
 
52
+ # Returns the number of items per page in the collection.
53
+ #
54
+ # @return [Integer] items per page
55
+ #
56
+ # @see Adapter::Base#per_page
15
57
  def per_page
16
- per_page_method = if represented.respond_to?(:per_page)
17
- :per_page
18
- else
19
- :limit_value
20
- end
21
-
22
- represented.public_send(per_page_method)
58
+ adapter.per_page
23
59
  end
24
60
 
61
+ # Returns the previous page of the collection.
62
+ #
63
+ # @return [Integer|NilClass] previous page number, if any
64
+ #
65
+ # @see Adapter::Base#previous_page
25
66
  def previous_page
26
- previous_page_method = if represented.respond_to?(:previous_page)
27
- :previous_page
28
- else
29
- :prev_page
30
- end
31
-
32
- represented.public_send(previous_page_method)
67
+ adapter.previous_page
33
68
  end
34
69
 
70
+ # Returns the total number of items in the collection.
71
+ #
72
+ # @return [Integer] number of items
73
+ #
74
+ # @see Adapter::Base#total_entries
35
75
  def total_entries
36
- total_entries_method = if represented.respond_to?(:total_entries)
37
- :total_entries
38
- else
39
- :total_count
40
- end
76
+ adapter.total_entries
77
+ end
78
+
79
+ # Returns the total number of pages in the collection.
80
+ #
81
+ # @return [Integer] number of pages
82
+ #
83
+ # @see Adapter::Base#total_pages
84
+ def total_pages
85
+ adapter.total_pages
86
+ end
87
+
88
+ private
41
89
 
42
- represented.public_send(total_entries_method)
90
+ def adapter
91
+ @adapter ||= Pagination::Adapter.load_for(represented)
43
92
  end
44
93
  end
45
94
 
@@ -49,7 +98,7 @@ module Pragma
49
98
  klass.class_eval do
50
99
  property :total_entries, exec_context: :decorator
51
100
  property :per_page, exec_context: :decorator
52
- property :total_pages
101
+ property :total_pages, exec_context: :decorator
53
102
  property :previous_page, exec_context: :decorator
54
103
  property :current_page, exec_context: :decorator
55
104
  property :next_page, exec_context: :decorator
@@ -4,13 +4,21 @@ module Pragma
4
4
  module Decorator
5
5
  # Supports rendering timestamps as UNIX times.
6
6
  #
7
- # @author Alessandro Desantis
7
+ # @example Rendering a timestamp as UNIX time
8
+ # class ArticleDecorator < Pragma::Decorator::Base
9
+ # timestamp :created_at
10
+ # end
11
+ #
12
+ # # {
13
+ # # "created_at": 1515250106
14
+ # # }
15
+ # ArticleDecorator.new(article).to_hash
8
16
  module Timestamp
9
17
  def self.included(klass)
10
18
  klass.extend ClassMethods
11
19
  end
12
20
 
13
- module ClassMethods # rubocop:disable Style/Documentation
21
+ module ClassMethods # :nodoc:
14
22
  # Defines a timestamp property which will be rendered as UNIX time.
15
23
  #
16
24
  # @param name [Symbol] the name of the property
@@ -34,7 +42,7 @@ module Pragma
34
42
 
35
43
  def create_timestamp_property(name, options = {})
36
44
  property "_#{name}_timestamp", options.merge(
37
- as: name,
45
+ as: options[:as] || name,
38
46
  exec_context: :decorator
39
47
  )
40
48
  end
@@ -4,28 +4,59 @@ module Pragma
4
4
  module Decorator
5
5
  # Adds a +type+ property containing the machine-readable type of the represented object.
6
6
  #
7
- # @author Alessandro Desantis
7
+ # This is useful for the client to understand what kind of resource it's dealing with
8
+ # and trigger related logic.
9
+ #
10
+ # @example Including the resource's type
11
+ # class ArticleDecorator < Pragma::Decorator::Base
12
+ # include Pragma::Decorator::Type
13
+ # end
14
+ #
15
+ # # {
16
+ # # "type": "article"
17
+ # # }
18
+ # ArticleDecorator.new(article).to_hash
8
19
  module Type
9
- TYPE_OVERRIDES = {
10
- array: 'list'
11
- }.freeze
20
+ class << self
21
+ def included(klass)
22
+ klass.class_eval do
23
+ property :type, exec_context: :decorator, render_nil: false
24
+ end
25
+ end
12
26
 
13
- def self.included(klass)
14
- klass.class_eval do
15
- property :type, exec_context: :decorator, render_nil: false
27
+ # Returns the type overrides.
28
+ #
29
+ # By default, +Array+ and +ActiveRecord::Relation+ are renamed to +list+.
30
+ #
31
+ # @return [Hash{String => String}] a hash of class-override pairs
32
+ def overrides
33
+ @overrides ||= {
34
+ 'Array' => 'list',
35
+ 'ActiveRecord::Relation' => 'list'
36
+ }
16
37
  end
17
38
  end
18
39
 
19
- # Returns the type of the decorated object (i.e. its underscored class name).
40
+ # Returns the type to expose to API clients.
41
+ #
42
+ # If an override is present for the decorated class, returns the override, otherwise returns
43
+ # the underscored class.
44
+ #
45
+ # @return [String] type to expose
20
46
  #
21
- # @return [String]
47
+ # @see .overrides
22
48
  def type
23
- type = decorated.class.name
49
+ Pragma::Decorator::Type.overrides[decorated.class.name] ||
50
+ underscore_klass(decorated.class.name)
51
+ end
52
+
53
+ private
54
+
55
+ def underscore_klass(klass)
56
+ klass
24
57
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
25
58
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
26
59
  .downcase
27
-
28
- TYPE_OVERRIDES[type.to_sym] || type
29
60
  end
30
61
  end
31
62
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Pragma
4
4
  module Decorator
5
- VERSION = '2.0.0'
5
+ VERSION = '2.1.0'
6
6
  end
7
7
  end