sozialhelden/rosemary

View on GitHub
lib/rosemary/api.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'httparty'
module Rosemary

  # The Api class handles all calls to the OpenStreetMap API.
  #
  # Usage:
  #   require 'rosemary'
  #   auth_client = Rosemary::BasicAuthClient.new(:user_name => 'user', :password => 'a_password')
  #   api = Rosemary::Api.new(auth_client)
  #   @node = api.find_node(1234)
  #   @node.tags << {:wheelchair => 'no'}
  #   @changeset = api.create_changeset('Set the wheelchair tag')
  #   api.save(@node, @changeset)
  class Api
    # Provide basic HTTP client behaviour.
    include HTTParty

    # The OSM API version supported by this gem.
    API_VERSION = "0.6".freeze

    # the default base URI for the API
    base_uri "https://www.openstreetmap.org"
    #base_uri "http://api06.dev.openstreetmap.org/api/#{API_VERSION}"

    # Make sure the request don't run forever
    default_timeout 5

    # Use a custom parser to handle the OSM XML format.
    parser Parser


    # @return [Rosemary::Client] the client to be used to authenticate the user towards the OSM API.
    attr_accessor :client

    # @return [Rosemary::Changeset] the current changeset to be used for all writing acces.
    attr_accessor :changeset

    # Creates an Rosemary::Api object with an optional client
    # @param [Rosemary::Client] client the client to authenticate the user for write access.
    def initialize(client=nil)
      @client = client
    end

    # Get a Node with specified ID from API.
    #
    # @param [Fixnum] id the id of the node to find.
    # @return [Rosemary::Node] the node with the given id.
    # @raise [Rosemary::Gone] in case the node has been deleted.
    def find_node(id)
      find_element('node', id)
    end

    # Get a Way with specified ID from API.
    #
    # @param [Fixnum] id the id of the node to find.
    # @return [Rosemary::Way] the way with the given id.
    #
    def find_way(id)
      find_element('way', id)
    end

    # Get a Relation with specified ID from API.
    #
    # call-seq: find_relation(id) -> Rosemary::Relation
    #
    def find_relation(id)
      find_element('relation', id)
    end

    # Get a Changeset with specified ID from API
    # if that changeset is missing, id is nil, or the changeset is closed
    # create a new one
    #
    # call-seq: find_or_create_open_changeset(id, comment) -> Rosemary::Changeset
    #
    def find_or_create_open_changeset(id, comment = nil, tags = {})
      find_open_changeset(id) || create_changeset(comment, tags)
    end

    def find_open_changeset(id)
      cs = find_changeset(id)
      (cs && cs.open?) ? cs : nil
    end

    # Get the user which represented by the Rosemary::Client
    #
    # @return: [Rosemary::User] user the user authenticated using the current client
    #
    def find_user
      raise CredentialsMissing if client.nil?
      resp = do_authenticated_request(:get, "/user/details")
      raise resp if resp.is_a? String
      resp
    end

    # Get the bounding box which is represented by the Rosemary::BoundingBox
    #
    # @param [Numeric] left is the longitude of the left (westernmost) side of the bounding box.
    # @param [Numeric] bottom is the latitude of the bottom (southernmost) side of the bounding box.
    # @param [Numeric] right is the longitude of the right (easternmost) side of the bounding box.
    # @param [Numeric] top is the latitude of the top (northernmost) side of the bounding box.
    # @return [Rosemary::BoundingBox] the bounding box containing all ways, nodes and relations inside the given coordinates
    #
    def find_bounding_box(left,bottom,right,top)
      do_request(:get, "/map?bbox=#{left},#{bottom},#{right},#{top}", {} )
    end


    def permissions
      if client.nil?
        get("/permissions")
      else
        do_authenticated_request(:get, "/permissions")
      end
    end

    # Deletes the given element using API write access.
    #
    # @param [Rosemary::Element] element the element to be created
    # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
    # @return [Fixnum] the new version of the deleted element.
    def destroy(element, changeset)
      element.changeset = changeset.id
      response = delete("/#{element.type.downcase}/#{element.id}", :body => element.to_xml) unless element.id.nil?
      response.to_i # New version number
    end

    # Creates or updates an element depending on the current state of persistance.
    #
    # @param [Rosemary::Element] element the element to be created
    # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
    def save(element, changeset)
      response = if element.id.nil?
        create(element, changeset)
      else
        update(element, changeset)
      end
    end

    # Create a new element using API write access.
    #
    # @param [Rosemary::Element] element the element to be created
    # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
    # @return [Fixnum] the id of the newly created element.
    def create(element, changeset)
      element.changeset = changeset.id
      put("/#{element.type.downcase}/create", :body => element.to_xml)
    end

    # Update an existing element using API write access.
    #
    # @param [Rosemary::Element] element the element to be created
    # @param [Rosemary::Changeset] changeset the changeset to be used to wrap the write access.
    # @return [Fixnum] the versiom of the updated element.
    def update(element, changeset)
      element.changeset = changeset.id
      response = put("/#{element.type.downcase}/#{element.id}", :body => element.to_xml)
      response.to_i # New Version number
    end

    # Create a new changeset with an optional comment
    #
    # @param [String] comment a meaningful comment for this changeset
    # @return [Rosemary::Changeset] the changeset which was newly created
    # @raise [Rosemary::NotFound] in case the changeset could not be found
    def create_changeset(comment = nil, tags = {})
      tags.merge!(:comment => comment) { |key, v1, v2| v1 }
      changeset = Changeset.new(:tags => tags)
      changeset_id = put("/changeset/create", :body => changeset.to_xml).to_i
      find_changeset(changeset_id) unless changeset_id == 0
    end

    # Get a Changeset with specified ID from API.
    #
    # @param [Integer] id the ID for the changeset you look for
    # @return [Rosemary::Changeset] the changeset which was found with the id
    def find_changeset(id)
      find_element('changeset', id)
    end

    # Closes the given changeset.
    #
    # @param [Rosemary::Changeset] changeset the changeset to be closed
    def close_changeset(changeset)
      put("/changeset/#{changeset.id}/close")
    end

    def find_changesets_for_user(options = {})
      user_id = find_user.id
      changesets = get("/changesets", :query => options.merge({:user => user_id}))
      changesets.nil? ? [] : changesets
    end

    # Get an object ('node', 'way', or 'relation') with specified ID from API.
    #
    # call-seq: find_element('node', id) -> Rosemary::Element
    #
    def find_element(type, id)
      raise ArgumentError.new("type needs to be one of 'node', 'way', and 'relation'") unless type =~ /^(node|way|relation|changeset)$/
      return nil if id.nil?
      begin
        response = get("/#{type}/#{id}")
        response.is_a?(Array ) ? response.first : response
      rescue NotFound
        nil
      end
    end

    # Create a note
    #
    # call-seq: create_note(lat: 51.00, lon: 0.1, text: 'Test note') -> Rosemary::Note
    #
    def create_note(note)
      post("/notes", :query => note)
    end

    private

    # most GET requests are valid without authentication, so this is the standard
    def get(url, options = {})
      do_request(:get, url, options)
    end

    # all PUT requests need authorization, so this is the stanard
    def put(url, options = {})
      do_authenticated_request(:put, url, options)
    end

    # all POST requests need authorization, so this is the stanard
    def post(url, options = {})
      do_authenticated_request(:post, url, options)
    end

    # all DELETE requests need authorization, so this is the stanard
    def delete(url, options = {})
      do_authenticated_request(:delete, url, options)
    end

    def api_url(url)
      "/api/#{API_VERSION}" + url
    end

    # Do a API request without authentication
    def do_request(method, url, options = {})
      begin
        response = self.class.send(method, api_url(url), options)
        check_response_codes(response)
        response.parsed_response
      rescue Timeout::Error
        raise Unavailable.new('Service Unavailable')
      end
    end

    # Do a API request with authentication, using the given client
    def do_authenticated_request(method, url, options = {})
      begin
        response = case client
                   when BasicAuthClient
                     self.class.send(method, api_url(url), options.merge(:basic_auth => client.credentials))
                   when OauthClient
                     # We have to wrap the result of the access_token request into an HTTParty::Response object
                     # to keep duck typing with HTTParty
                     result = client.send(method, api_url(url), options)
                     content_type = Parser.format_from_mimetype(result.content_type)
                     parsed_response = Parser.call(result.body, content_type)

                     HTTParty::Response.new(nil, result, lambda { parsed_response })
                   else
                     raise CredentialsMissing
                   end
        check_response_codes(response)
        response.parsed_response
      rescue Timeout::Error
        raise Unavailable.new('Service Unavailable')
      end
    end

    def check_response_codes(response)
      body = response.body
      case response.code.to_i
      when 200 then return
      when 400 then raise BadRequest.new(body)
      when 401 then raise Unauthorized.new(body)
      when 403 then raise Forbidden.new(body)
      when 404 then raise NotFound.new(body)
      when 405 then raise MethodNotAllowed.new(body)
      when 409 then raise Conflict.new(body)
      when 410 then raise Gone.new(body)
      when 412 then raise Precondition.new(body)
#      when 414 then raise UriTooLarge.new(body)
      when 500 then raise ServerError, 'Internal Server Error'
      when 503 then raise Unavailable, 'Service Unavailable'
      else raise "Unknown response code: #{response.code}"
      end
    end

  end
end