base.rb 9.6 KB
Newer Older
1
require 'active_resource/connection'
2 3
require 'cgi'
require 'set'
4 5 6

module ActiveResource
  class Base
7
    # The logger for diagnosing and tracing ARes calls.
8 9
    cattr_accessor :logger

10
    class << self
11
      # Gets the URI of the resource's site
12 13 14 15 16 17 18
      def site
        if defined?(@site)
          @site
        elsif superclass != Object and superclass.site
          superclass.site.dup.freeze
        end
      end
J
Jeremy Kemper 已提交
19

20
      # Set the URI for the REST resources
21
      def site=(site)
22
        @connection = nil
23
        @site = create_site_uri_from(site)
24 25
      end

26
      # Base connection to remote service
27 28 29 30
      def connection(refresh = false)
        @connection = Connection.new(site) if refresh || @connection.nil?
        @connection
      end
31

32 33 34 35 36 37
      attr_accessor_with_default(:element_name)    { to_s.underscore } #:nodoc:
      attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
      attr_accessor_with_default(:primary_key, 'id') #:nodoc:
      
      # Gets the resource prefix
      #  prefix/collectionname/1.xml
38 39 40
      def prefix(options={})
        default = site.path
        default << '/' unless default[-1..-1] == '/'
41
        self.prefix = default
42 43
        prefix(options)
      end
44

45 46
      # Sets the resource prefix
      #  prefix/collectionname/1.xml
47
      def prefix=(value = '/')
48
        # Replace :placeholders with '#{embedded options[:lookups]}'
49
        prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
50 51 52

        # Redefine the new methods.
        code = <<-end_code
53 54
          def prefix_source() "#{value}" end
          def prefix(options={}) "#{prefix_call}" end
55 56
        end_code
        silence_warnings { instance_eval code, __FILE__, __LINE__ }
57
      rescue
58
        logger.error "Couldn't set prefix: #{$!}\n  #{code}"
59
        raise
60
      end
61

62
      alias_method :set_prefix, :prefix=  #:nodoc:
63

64 65
      alias_method :set_element_name, :element_name=  #:nodoc:
      alias_method :set_collection_name, :collection_name=  #:nodoc:
66 67

      def element_path(id, options = {})
68
        "#{prefix(options)}#{collection_name}/#{id}.xml#{query_string(options)}"
69
      end
70

71
      def collection_path(options = {}) 
72
        "#{prefix(options)}#{collection_name}.xml#{query_string(options)}"
73
      end
74

75
      alias_method :set_primary_key, :primary_key=  #:nodoc:
76

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
      # Create a new resource instance and request to the remote service
      # that it be saved.  This is equivalent to the following simultaneous calls:
      #
      #   ryan = Person.new(:first => 'ryan')
      #   ryan.save
      #
      # The newly created resource is returned.  If a failure has occurred an
      # exception will be raised (see save).  If the resource is invalid and
      # has not been saved then <tt>resource.valid?</tt> will return <tt>false</tt>,
      # while <tt>resource.new?</tt> will still return <tt>true</tt>.
      #      
      def create(attributes = {}, prefix_options = {})
        returning(self.new(attributes, prefix_options)) { |res| res.save }        
      end

92 93 94
      # Core method for finding resources.  Used similarly to ActiveRecord's find method.
      #  Person.find(1) # => GET /people/1.xml
      #  StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml
95
      def find(*arguments)
96 97
        scope   = arguments.slice!(0)
        options = arguments.slice!(0) || {}
98 99

        case scope
100 101 102
          when :all   then find_every(options)
          when :first then find_every(options).first
          else             find_single(scope, options)
103 104
        end
      end
105

106 107 108 109
      def delete(id)
        connection.delete(element_path(id))
      end

110
      # Evalutes to <tt>true</tt> if the resource is found.
111 112 113 114 115 116
      def exists?(id, options = {})
        id && !find_single(id, options).nil?
      rescue ActiveResource::ResourceNotFound
        false
      end

117
      private
118
        # Find every resource.
119
        def find_every(options)
120 121
          collection = connection.get(collection_path(options)) || []
          collection.collect! { |element| new(element, options) }
122
        end
123

124 125
        # Find a single resource.
        #  { :person => person1 }
126
        def find_single(scope, options)
127
          new(connection.get(element_path(scope, options)), options)
128
        end
129

130
        # Accepts a URI and creates the site URI from that.
131
        def create_site_uri_from(site)
132
          site.is_a?(URI) ? site.dup : URI.parse(site)
133
        end
134

135 136 137 138
        def prefix_parameters
          @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
        end

139
        # Builds the query string for the request.
140 141
        def query_string(options)
          # Omit parameters which appear in the URI path.
142
          query_params = options.reject { |key, value| prefix_parameters.include?(key) }
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160

          # Accumulate a list of escaped key=value pairs for the given parameters.
          pairs = []
          query_params.each do |key, value|
            key = CGI.escape(key.to_s)

            # a => b becomes a=b
            # a => [b, c] becomes a[]=b&a[]=c
            case value
              when Array
                value.each { |val| pairs << "#{key}[]=#{CGI.escape(val.to_s)}" }
              else
                pairs << "#{key}=#{CGI.escape(value.to_s)}"
            end
          end

          "?#{pairs * '&'}" unless pairs.empty?
        end
161 162
    end

163 164
    attr_accessor :attributes #:nodoc:
    attr_accessor :prefix_options #:nodoc:
165

166
    def initialize(attributes = {}, prefix_options = {})
167 168
      @attributes = {}
      self.load attributes
169
      @prefix_options = prefix_options
170
    end
171

172
    # Is the resource a new object?
173
    def new?
174 175 176
      id.nil?
    end

177
    # Get the id of the object.
178
    def id
179
      attributes[self.class.primary_key]
180
    end
181

182
    # Set the id of the object.
183
    def id=(id)
184
      attributes[self.class.primary_key] = id
185
    end
186

187
    # True if and only if +other+ is the same object or is an instance of the same class, is not +new?+, and has the same +id+.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    def ==(other)
      other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
    end

    # Delegates to ==
    def eql?(other)
      self == other
    end

    # Delegates to id in order to allow two resources of the same type and id to work with something like:
    #   [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
    def hash
      id.hash
    end

203
    # Delegates to +create+ if a new object, +update+ if its old.
204
    def save
205
      new? ? create : update
206 207
    end

208
    # Delete the resource.
209
    def destroy
210
      connection.delete(element_path)
211
    end
212

213
    # Evaluates to <tt>true</tt> if this resource is found.
214 215 216 217
    def exists?
      !new? && self.class.exists?(id, prefix_options)
    end

218
    # Convert the resource to an XML string
219 220
    def to_xml(options={})
      attributes.to_xml({:root => self.class.element_name}.merge(options))
221
    end
222 223 224

    # Reloads the attributes of this object from the remote web service.
    def reload
225 226 227 228 229 230
      self.load self.class.find(id, @prefix_options).attributes
    end

    # Manually load attributes from a hash. Recursively loads collections of
    # resources.
    def load(attributes)
J
Jeremy Kemper 已提交
231
      raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
232 233 234 235 236 237 238
      attributes.each do |key, value|
        @attributes[key.to_s] =
          case value
            when Array
              resource = find_or_create_resource_for_collection(key)
              value.map { |attrs| resource.new(attrs) }
            when Hash
239 240
              resource = find_or_create_resource_for(key)
              resource.new(value)
241
            when ActiveResource::Base
242
              value.class.new(value.attributes, value.prefix_options)
243 244 245 246
            else
              value.dup rescue value
          end
      end
247 248 249
      self
    end

250 251 252 253
    protected
      def connection(refresh = false)
        self.class.connection(refresh)
      end
254

255
      # Update the resource on the remote service.
256
      def update
257
        connection.put(element_path, to_xml)
258
      end
259

260
      # Create (i.e., save to the remote service) the new resource.
261
      def create
262 263 264
        returning connection.post(collection_path, to_xml) do |response|
          self.id = id_from_response(response)
        end
265 266
      end

267
      # Takes a response from a typical create post and pulls the ID out
268 269 270 271
      def id_from_response(response)
        response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
      end

272 273 274 275 276 277 278 279
      def element_path(options = nil)
        self.class.element_path(id, options || prefix_options)
      end

      def collection_path(options = nil)
        self.class.collection_path(options || prefix_options)
      end

280
    private
281
      # Tries to find a resource for a given collection name; if it fails, then the resource is created
282 283 284
      def find_or_create_resource_for_collection(name)
        find_or_create_resource_for(name.to_s.singularize)
      end
285 286
      
      # Tries to find a resource for a given name; if it fails, then the resource is created
287 288 289 290 291 292
      def find_or_create_resource_for(name)
        resource_name = name.to_s.camelize
        resource_name.constantize
      rescue NameError
        resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
        resource.prefix = self.class.prefix
293
        resource.site   = self.class.site
294 295 296
        resource
      end

297
      def method_missing(method_symbol, *arguments) #:nodoc:
298
        method_name = method_symbol.to_s
299

300 301 302 303
        case method_name.last
          when "="
            attributes[method_name.first(-1)] = arguments.first
          when "?"
304
            attributes[method_name.first(-1)] == true
305
          else
306
            attributes.has_key?(method_name) ? attributes[method_name] : super
307 308 309
        end
      end
  end
J
Jeremy Kemper 已提交
310
end