File: lazy_accessors.rb

package info (click to toggle)
ruby-puppet-forge 6.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,220 kB
  • sloc: ruby: 2,397; makefile: 3
file content (133 lines) | stat: -rw-r--r-- 4,546 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
module PuppetForge

  # When dealing with a remote service, it's reasonably common to receive only
  # a partial representation of the underlying object, with additional data
  # available upon request. PuppetForge, by default, provides a convenient interface
  # for accessing whatever local data is available, but lacks good support for
  # fleshing out partial representations. In order to build a seamless
  # interface for both local and remote attriibutes, this module replaces the
  # default behavior with an "updatable" interface.
  module LazyAccessors

    # Callback for module inclusion.
    #
    # On each lazy class we'll store a reference to a Module, which will act as
    # the container for the attribute methods.
    #
    # @param base [Class] the Class this module was included into
    # @return [void]
    def self.included(base)
      base.singleton_class.class_eval do
        attr_accessor :_accessor_container
      end
    end

    # Provide class name for object
    #
    def class_name
      self.class.name.split("::").last.downcase
    end

    # Override the default #inspect behavior.
    #
    # The original behavior actually invokes each attribute accessor, which can
    # be somewhat problematic when the accessors have been overridden. This
    # implementation simply reports the contents of the attributes hash.
    def inspect
      attrs = attributes.map do |x, y|
        [ x, y ].join('=')
      end
      "#<#{self.class}(#{uri}) #{attrs.join(' ')}>"
    end

    # Override the default #method_misssing behavior.
    #
    # When we receive a {#method_missing} call, one of three things is true:
    # - the caller is looking up a piece of local data without an accessor
    # - the caller is looking up a piece of remote data
    # - the method doesn't actually exist
    #
    # To solve the remote case, we begin by ensuring our attribute list is
    # up-to-date with a call to {#fetch}, create a new {AccessorContainer} if
    # necessary, and add any missing accessors to the container. We can then
    # dispatch the method call to the newly created accessor.
    #
    # The local case is almost identical, except that we can skip updating the
    # model's attributes.
    #
    # If, after our work, we haven't seen the requested method name, we can
    # surmise that it doesn't actually exist, and pass the call along to
    # upstream handlers.
    def method_missing(name, *args, &blk)
      fetch unless has_attribute?(name.to_s[/\w+/])

      klass = self.class
      mod = (klass._accessor_container ||= AccessorContainer.new(klass))
      mod.add_attributes(attributes.keys)

      if (meth = mod.instance_method(name) rescue nil)
        return meth.bind(self).call(*args)
      else
        return super(name, *args, &blk)
      end
    end

    # Updates model data from the API. This method will short-circuit if this
    # model has already been fetched from the remote server, to avoid duplicate
    # requests.
    #
    # @return [self]
    def fetch
      return self if @_fetch

      klass = self.class

      response = klass.request("#{self.class_name}s/#{self.slug}")
      if @_fetch = response.success?
        self.send(:initialize, response.body)
      end

      return self
    end

    # A Module subclass for attribute accessors.
    class AccessorContainer < Module

      # Creating a new instance of this class will automatically include itself
      # into the provided class.
      #
      # @param base [Class] the class this container belongs to
      def initialize(base)
        base.send(:include, self)
      end

      # Adds attribute accessors, predicates, and mutators for the named keys.
      # Since these methods will also be instantly available on all instances
      # of the parent class, each of these methods will also conservatively
      # {LazyAccessors#fetch} any missing data.
      #
      # @param keys [Array<#to_s>] the list of attributes to create
      # @return [void]
      def add_attributes(keys)
        keys.each do |key|
          next if methods.include?(name = key)

          define_method("#{name}") do
            fetch unless has_attribute?(name)
            attribute(name)
          end

          define_method("#{name}?") do
            fetch unless has_attribute?(name)
            has_attribute?(name)
          end

          define_method("#{name}=") do |value|
            fetch unless has_attribute?(name)
            attributes[name] = value
          end
        end
      end
    end
  end
end