pragma-decorator 2.0.0 → 2.1.0

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