pax_global_header00006660000000000000000000000064142275603200014513gustar00rootroot0000000000000052 comment=77a10a5764c335c5f6a18f0f180ec4e0d7426bc8 lostisland-sawyer-77a10a5/000077500000000000000000000000001422756032000155265ustar00rootroot00000000000000lostisland-sawyer-77a10a5/.github/000077500000000000000000000000001422756032000170665ustar00rootroot00000000000000lostisland-sawyer-77a10a5/.github/workflows/000077500000000000000000000000001422756032000211235ustar00rootroot00000000000000lostisland-sawyer-77a10a5/.github/workflows/ci.yml000066400000000000000000000013151422756032000222410ustar00rootroot00000000000000name: CI on: push: branches: [ master ] pull_request: env: GIT_COMMIT_SHA: ${{ github.sha }} GIT_BRANCH: ${{ github.ref }} jobs: build: runs-on: ubuntu-latest env: FARADAY_VERSION: ${{ matrix.faraday }} strategy: fail-fast: false matrix: ruby: [ '2.6', '2.7', '3.0', '3.1', 'truffleruby', 'jruby' ] faraday: [ '~> 0.17.3', '~> 1.0', '~> 2.0' ] steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Test continue-on-error: ${{ contains(fromJson('["truffleruby", "jruby"]'), matrix.ruby) }} run: bundle exec rake lostisland-sawyer-77a10a5/.gitignore000066400000000000000000000001041422756032000175110ustar00rootroot00000000000000.bundle .ruby-version bin Gemfile.lock pkg vendor/cache vendor/gems lostisland-sawyer-77a10a5/CONTRIBUTING.md000066400000000000000000000012201422756032000177520ustar00rootroot00000000000000## Submitting a Pull Request 1. [Fork the repository.][fork] 2. [Create a topic branch.][branch] 3. Add specs for your unimplemented feature or bug fix. 4. Run `script/test`. If your specs pass, return to step 3. 5. Implement your feature or bug fix. 6. Run `script/test`. If your specs fail, return to step 5. 7. Add, commit, and push your changes. For documentation-only fixes, please add "[ci skip]" to your commit message to avoid needless CI builds. 8. [Submit a pull request.][pr] [fork]: https://help.github.com/articles/fork-a-repo [branch]: http://learn.github.com/p/branching.html [pr]: https://help.github.com/articles/using-pull-requests lostisland-sawyer-77a10a5/Gemfile000066400000000000000000000002601422756032000170170ustar00rootroot00000000000000source "http://rubygems.org" gemspec gem "rake" group :test do gem "minitest" end install_if -> { ENV["FARADAY_VERSION"] } do gem "faraday", ENV["FARADAY_VERSION"] end lostisland-sawyer-77a10a5/LICENSE.md000066400000000000000000000020361422756032000171330ustar00rootroot00000000000000Copyright (c) 2011 rick olson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lostisland-sawyer-77a10a5/README.md000066400000000000000000000031031422756032000170020ustar00rootroot00000000000000# Sawyer Sawyer is an experimental hypermedia agent for Ruby built on top of [Faraday][faraday]. [faraday]: https://github.com/lostisland/faraday ## Installation Add this line to your application's Gemfile: ```ruby gem 'sawyer' ``` And then execute: ```sh bundle ``` Or install it yourself as: ```sh gem install sawyer ``` ## Usage ```ruby require "sawyer" # Create a Sawyer agent agent = Sawyer::Agent.new("https://api.github.com", links_parser: Sawyer::LinkParsers::Simple.new) # Fetch the root of the API root = agent.root.data # Access a resource directly contributors = agent.call(:get, "repos/lostisland/sawyer/contributors").data # Load a hypermedia relation top_contributor = contributors.first followers = top_contributor.rels[:followers].get.data ``` For more information, check out the [documentation](http://www.rubydoc.info/gems/sawyer/). ## Development After checking out the repo, run `script/test` to bootstrap the project and run the tests. You can also run `script/console` for an interactive prompt that will allow you to experiment. To package the gem, run `script/package`. To release a new version, update the version number in [`lib/sawyer.rb`](lib/sawyer.rb), and then run `script/release`, which will create a git tag for the version, push git commits and tags, and push the .gem file to [rubygems.org](https://rubygems.org). ## Contributing Check out the [contributing guide](CONTRIBUTING.md) for more information on contributing. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). lostisland-sawyer-77a10a5/Rakefile000066400000000000000000000003261422756032000171740ustar00rootroot00000000000000require 'rubygems' require 'rake' task :default => :test require 'rake/testtask' Rake::TestTask.new(:test) do |test| test.libs << 'lib' << 'test' test.pattern = 'test/**/*_test.rb' test.verbose = true end lostisland-sawyer-77a10a5/SPEC.md000066400000000000000000000025311422756032000166030ustar00rootroot00000000000000# Sawyer To use Sawyer, create a new Agent with a URL endpoint. ```ruby endpoint = "http://my-api.com" agent = Sawyer::Agent.new endpoint do |conn| conn.headers['content-type'] = 'application/vnd.my-api+json' end ``` From here, we can access the root to get the initial actions. ```ruby root = agent.start ``` Every request in Sawyer returns a Sawyer::Response. It's very similar to a raw Faraday::Response, with a few extra methods. ```ruby # HTTP Headers root.headers # HTTP status root.status # The JSON Schema root.schema # The link relations root.rels # The contents (probably empty from the root) root.data ``` Now, we can access a relation off the root resource. ```ruby resource = root.data res = resource.rels[:users].call :query => {:sort => 'login'} # An array of users users = res.data ``` This call returns two types of relations: relations on the collection of users, and relations on each user. You can access the collection resources from the response. ```ruby # get the next page of users res2 = res.rels[:next].call # page 2 of the users collection res2.data ``` Each user has it's own relations too: ```ruby # favorite the previous user users.each_with_index do |user, index| res = user.rels[:favorite].call users[index-1] if !res.success? puts "#{user.name} could not favorite #{users[index-1].name}" end end ``` lostisland-sawyer-77a10a5/example/000077500000000000000000000000001422756032000171615ustar00rootroot00000000000000lostisland-sawyer-77a10a5/example/client.rb000066400000000000000000000016711422756032000207710ustar00rootroot00000000000000require File.expand_path("../../lib/sawyer", __FILE__) require 'faraday' require 'pp' endpoint = "http://localhost:4567/" agent = Sawyer::Agent.new(endpoint) do |http| http.headers['content-type'] = 'application/json' end puts agent.inspect puts root = agent.start puts root.inspect puts "LISTING USERS" users_rel = root.data.rels[:users] puts users_rel.inspect puts users_res = users_rel.get puts users_res.inspect users = users_res.data users.each do |user| puts "#{user.login} favorites:" fav_res = user.rels[:favorites].get fav_res.data.each do |sushi| puts "- #{sushi.inspect})" end puts end puts "CREATING USER" create_user_rel = root.data.rels[:users] puts create_user_rel.inspect created = create_user_rel.post(:login => 'booya') puts created.inspect puts puts "ADD A FAVORITE" created_user = created.data create_fav_res = created_user.rels[:favorites].post nil, :query => {:id => 1} puts create_fav_res.inspect lostisland-sawyer-77a10a5/example/nigiri.schema.json000066400000000000000000000015251422756032000225770ustar00rootroot00000000000000{ "type": "object", "relations": [ {"rel": "all", "href": "/nigiri"} ], "properties": { "id": { "type": "integer", "minimum": 1, "readonly": true }, "name": { "type": "string" }, "fish": { "type": "string" }, "_links": { "type": "array", "items": { "type": "object", "properties": { "rel": { "type": "string" }, "href": { "type": "string", "optional": true }, "method": { "type": "string", "default": "get" }, "schema": { "type": "string", "optional": true } }, "additionalProperties": false }, "additionalProperties": false, "readonly": true } } } lostisland-sawyer-77a10a5/example/server.rb000066400000000000000000000043521422756032000210200ustar00rootroot00000000000000require 'sinatra' require 'json' get '/' do app_type body JSON.dump({ :_links => { :users => {:href => "/users", :method => 'get,post'}, :nigiri => {:href => "/nigiri"} } }) end def app_type content_type "application/vnd.sushihub+json" end users = [ {:id => 1, :login => 'sawyer', :created_at => Time.utc(2004, 9, 22), :_links => { :self => {:href => '/users/sawyer'}, :favorites => {:href => '/users/sawyer/favorites', :method => 'get,post'} }}, {:id => 2, :login => 'faraday', :created_at => Time.utc(2004, 12, 22), :_links => { :self => {:href => '/users/faraday'}, :favorites => {:href => '/users/faraday/favorites', :method => 'get,post'} }} ] nigiri = [ {:id => 1, :name => 'sake', :fish => 'salmon', :_links => { :self => {:href => '/nigiri/sake'} }}, {:id => 2, :name => 'unagi', :fish => 'eel', :_links => { :self => {:href => '/nigiri/unagi'} }} ] get '/users' do app_type body JSON.dump users end new_users = {} post '/users' do if env['CONTENT_TYPE'].to_s !~ /json/i halt 400, "Needs JSON" end app_type hash = JSON.load request.body.read new_users[hash[:login]] = hash headers "Location" => "/users/#{hash[:login]}" status 201 body JSON.dump hash.update( :id => 3, :created_at => Time.now.utc.xmlschema, :_links => { :self => {:href => "/users/#{hash[:login]}"}, :favorites => {:href => "/users/#{hash[:login]}/favorites", :method => 'get,post'} } ) end get '/users/:login' do headers 'Content-Type' => app_type if hash = users.detect { |u| u[:login] == params[:login] } body JSON.dump hash else halt 404 end end get '/users/:login/favorites' do app_type case params[:login] when users[0][:login] then body JSON.dump([nigiri[0]]) when users[1][:login] then body JSON.dump([]) else halt 404 end end post '/users/:login/favorites' do if params[:id].to_i > 0 halt 201 else halt 422 end end get '/nigiri' do app_type body JSON.dump nigiri end get '/nigiri/:name' do app_type if hash = nigiri.detect { |n| n[:name] == params[:name] } body JSON.dump hash else halt(404) end end lostisland-sawyer-77a10a5/example/user.schema.json000066400000000000000000000020711422756032000222710ustar00rootroot00000000000000{ "type": "object", "relations": [ {"rel": "all", "href": "/users"}, {"rel": "create", "href": "/users", "method": "post"}, {"rel": "favorites", "schema": "/schema/nigiri"}, {"rel": "favorites/create", "method": "post"} ], "properties": { "id": { "type": "integer", "minimum": 1, "readonly": true }, "login": { "type": "string" }, "created_at": { "type": "string", "pattern": "\\d{8}T\\d{6}Z", "readonly": true }, "_links": { "type": "array", "items": { "type": "object", "properties": { "rel": { "type": "string" }, "href": { "type": "string", "optional": true }, "method": { "type": "string", "default": "get" }, "schema": { "type": "string", "optional": true } }, "additionalProperties": false }, "additionalProperties": false, "readonly": true } } } lostisland-sawyer-77a10a5/lib/000077500000000000000000000000001422756032000162745ustar00rootroot00000000000000lostisland-sawyer-77a10a5/lib/sawyer.rb000066400000000000000000000004011422756032000201260ustar00rootroot00000000000000module Sawyer VERSION = "0.9.1" class Error < StandardError; end end require 'set' %w( resource relation response serializer agent link_parsers/hal link_parsers/simple ).each { |f| require File.expand_path("../sawyer/#{f}", __FILE__) } lostisland-sawyer-77a10a5/lib/sawyer/000077500000000000000000000000001422756032000176065ustar00rootroot00000000000000lostisland-sawyer-77a10a5/lib/sawyer/agent.rb000066400000000000000000000107601422756032000212350ustar00rootroot00000000000000require 'faraday' require 'addressable/template' module Sawyer class Agent NO_BODY = Set.new([:get, :head]) attr_accessor :links_parser attr_accessor :allow_undefined_methods class << self attr_writer :serializer end def self.serializer @serializer ||= Serializer.any_json end def self.encode(data) serializer.encode(data) end def self.decode(data) serializer.decode(data) end # Agents handle making the requests, and passing responses to # Sawyer::Response. # # endpoint - String URI of the API entry point. # options - Hash of options. # :allow_undefined_methods - Allow relations to call all the HTTP verbs, # not just the ones defined. # :faraday - Optional Faraday::Connection to use. # :links_parser - Optional parser to parse link relations # Defaults: Sawyer::LinkParsers::Hal.new # :serializer - Optional serializer Class. Defaults to # self.serializer_class. # # Yields the Faraday::Connection if a block is given. def initialize(endpoint, options = nil) @endpoint = endpoint @conn = (options && options[:faraday]) || Faraday.new @serializer = (options && options[:serializer]) || self.class.serializer @links_parser = (options && options[:links_parser]) || Sawyer::LinkParsers::Hal.new @allow_undefined_methods = (options && options[:allow_undefined_methods]) @conn.url_prefix = @endpoint yield @conn if block_given? end # Public: Close the underlying connection. def close @conn.close if @conn.respond_to?(:close) end # Public: Retains a reference to the root relations of the API. # # Returns a Sawyer::Relation::Map. def rels @rels ||= root.data._rels end # Public: Retains a reference to the root response of the API. # # Returns a Sawyer::Response. def root @root ||= start end # Public: Hits the root of the API to get the initial actions. # # Returns a Sawyer::Response. def start call :get, @endpoint end # Makes a request through Faraday. # # method - The Symbol name of an HTTP method. # url - The String URL to access. This can be relative to the Agent's # endpoint. # data - The Optional Hash or Resource body to be sent. :get or :head # requests can have no body, so this can be the options Hash # instead. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # # Returns a Sawyer::Response. def call(method, url, data = nil, options = nil) if NO_BODY.include?(method) options ||= data data = nil end options ||= {} url = expand_url(url, options[:uri]) started = nil res = @conn.send method, url do |req| if data req.body = data.is_a?(String) ? data : encode_body(data) end if params = options[:query] req.params.update params end if headers = options[:headers] req.headers.update headers end started = Time.now end Response.new self, res, :sawyer_started => started, :sawyer_ended => Time.now end # Encodes an object to a string for the API request. # # data - The Hash or Resource that is being sent. # # Returns a String. def encode_body(data) @serializer.encode(data) end # Decodes a String response body to a resource. # # str - The String body from the response. # # Returns an Object resource (Hash by default). def decode_body(str) @serializer.decode(str) end def parse_links(data) @links_parser.parse(data) end def expand_url(url, options = nil) tpl = url.respond_to?(:expand) ? url : Addressable::Template.new(url.to_s) tpl.expand(options || {}).to_s end def allow_undefined_methods? !!@allow_undefined_methods end def inspect %(<#{self.class} #{@endpoint}>) end # private def to_yaml_properties [:@endpoint] end def marshal_dump [@endpoint] end def marshal_load(dumped) @endpoint = *dumped.shift(1) end end end lostisland-sawyer-77a10a5/lib/sawyer/link_parsers/000077500000000000000000000000001422756032000223025ustar00rootroot00000000000000lostisland-sawyer-77a10a5/lib/sawyer/link_parsers/hal.rb000066400000000000000000000002501422756032000233700ustar00rootroot00000000000000module Sawyer module LinkParsers class Hal def parse(data) links = data.delete(:_links) return data, links end end end end lostisland-sawyer-77a10a5/lib/sawyer/link_parsers/simple.rb000066400000000000000000000011221422756032000241140ustar00rootroot00000000000000module Sawyer module LinkParsers class Simple LINK_REGEX = /_?url$/ # Public: Parses simple *_url style links on resources # # data - Hash of resource data # # Returns a Hash of data with separate links Hash def parse(data) links = {} inline_links = data.keys.select {|k| k.to_s[LINK_REGEX] } inline_links.each do |key| rel_name = key.to_s == 'url' ? 'self' : key.to_s.gsub(LINK_REGEX, '') links[rel_name.to_sym] = data[key] end return data, links end end end end lostisland-sawyer-77a10a5/lib/sawyer/relation.rb000066400000000000000000000206251422756032000217550ustar00rootroot00000000000000module Sawyer class Relation class Map # Tracks the available next actions for a resource, and # issues requests for them. def initialize @map = {} end # Adds a Relation to the map. # # rel - A Relation. # # Returns nothing. def <<(rel) @map[rel.name] = rel if rel end # Gets the raw Relation by its name. # # key - The Symbol name of the Relation. # # Returns a Relation. def [](key) @map[key.to_sym] end # Gets the number of mapped Relations. # # Returns an Integer. def size @map.size end # Gets a list of the Relation names. # # Returns an Array of Symbols in no specific order. def keys @map.keys end def to_hash pairs = @map.map do |k, v| [(k.to_s + "_url").to_sym, v.href] end Hash[pairs] end alias :to_h :to_hash def inspect hash = to_hash hash.respond_to?(:pretty_inspect) ? hash.pretty_inspect : hash.inspect end end attr_reader :agent, :name, :href_template, :method, :available_methods # Public: Builds an index of Relations from the value of a `_links` # property in a resource. :get is the default method. Any links with # multiple specified methods will get multiple relations created. # # index - The Hash mapping Relation names to the Hash Relation # options. # rels - A Relation::Map to store the Relations. # # Returns a Relation::Map def self.from_links(agent, index, rels = Map.new) if index.is_a?(Array) raise ArgumentError, "Links must be a hash of rel => {_href => '...'}: #{index.inspect}" end index.each do |name, options| rels << from_link(agent, name, options) end if index rels end # Public: Builds a single Relation from the given options. These are # usually taken from a `_links` property in a resource. # # agent - The Sawyer::Agent that made the request. # name - The Symbol name of the Relation. # options - A Hash containing the other Relation properties. # :href - The String URL of the next action's location. # :method - The optional String HTTP method. # # Returns a Relation. def self.from_link(agent, name, options) case options when Hash new agent, name, options[:href], options[:method] when String new agent, name, options end end # A Relation represents an available next action for a resource. # # agent - The Sawyer::Agent that made the request. # name - The Symbol name of the relation. # href - The String URL of the location of the next action. # method - The Symbol HTTP method. Default: :get def initialize(agent, name, href, method = nil) @agent = agent @name = name.to_sym @href = href @href_template = Addressable::Template.new(href.to_s) methods = nil if method.is_a? String if method.size.zero? method = nil else method.downcase! methods = method.split(',').map! do |m| m.strip! m.to_sym end method = methods.first end end @method = (method || :get).to_sym @available_methods = Set.new methods || [@method] end # Public: Makes an API request with the curent Relation using HEAD. # # data - The Optional Hash or Resource body to be sent. :get or :head # requests can have no body, so this can be the options Hash # instead. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def head(options = nil) options ||= {} options[:method] = :head call options end # Public: Makes an API request with the curent Relation using GET. # # data - The Optional Hash or Resource body to be sent. :get or :head # requests can have no body, so this can be the options Hash # instead. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def get(options = nil) options ||= {} options[:method] = :get call options end # Public: Makes an API request with the curent Relation using POST. # # data - The Optional Hash or Resource body to be sent. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def post(data = nil, options = nil) options ||= {} options[:method] = :post call data, options end # Public: Makes an API request with the curent Relation using PUT. # # data - The Optional Hash or Resource body to be sent. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def put(data = nil, options = nil) options ||= {} options[:method] = :put call data, options end # Public: Makes an API request with the curent Relation using PATCH. # # data - The Optional Hash or Resource body to be sent. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def patch(data = nil, options = nil) options ||= {} options[:method] = :patch call data, options end # Public: Makes an API request with the curent Relation using DELETE. # # data - The Optional Hash or Resource body to be sent. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def delete(data = nil, options = nil) options ||= {} options[:method] = :delete call data, options end # Public: Makes an API request with the curent Relation using OPTIONS. # # data - The Optional Hash or Resource body to be sent. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Returns a Sawyer::Response. def options(data = nil, opt = nil) opt ||= {} opt[:method] = :options call data, opt end def href(options = nil) return @href if @href_template.nil? @href_template.expand(options || {}).to_s end # Public: Makes an API request with the curent Relation. # # data - The Optional Hash or Resource body to be sent. :get or :head # requests can have no body, so this can be the options Hash # instead. # options - Hash of option to configure the API request. # :headers - Hash of API headers to set. # :query - Hash of URL query params to set. # :method - Symbol HTTP method. # # Raises ArgumentError if the :method value is not in @available_methods. # Returns a Sawyer::Response. def call(data = nil, options = nil) m = options && options[:method] if m && !@agent.allow_undefined_methods? && !@available_methods.include?(m == :head ? :get : m) raise ArgumentError, "method #{m.inspect} is not available: #{@available_methods.to_a.inspect}" end @agent.call m || @method, @href_template, data, options end def inspect %(#<#{self.class}: #{@name}: #{@method} #{@href_template}>) end end end lostisland-sawyer-77a10a5/lib/sawyer/resource.rb000066400000000000000000000077571422756032000220020ustar00rootroot00000000000000module Sawyer class Resource SPECIAL_METHODS = Set.new(%w(agent rels fields)) attr_reader :_agent, :_rels, :_fields attr_reader :attrs include Enumerable # Initializes a Resource with the given data. # # agent - The Sawyer::Agent that made the API request. # data - Hash of key/value properties. def initialize(agent, data = {}) @_agent = agent data, links = agent.parse_links(data) @_rels = Relation.from_links(agent, links) @_fields = Set.new @_metaclass = (class << self; self; end) @attrs = {} data.each do |key, value| @_fields << key @attrs[key.to_sym] = process_value(value) end @_metaclass.send(:attr_accessor, *data.keys) end # Processes an individual value of this resource. Hashes get exploded # into another Resource, and Arrays get their values processed too. # # value - An Object value of a Resource's data. # # Returns an Object to set as the value of a Resource key. def process_value(value) case value when Hash then self.class.new(@_agent, value) when Array then value.map { |v| process_value(v) } else value end end # Checks to see if the given key is in this resource. # # key - A Symbol key. # # Returns true if the key exists, or false. def key?(key) @_fields.include? key end # Allow fields to be retrieved via Hash notation # # method - key name # # Returns the value from attrs if exists def [](method) send(method.to_sym) rescue NoMethodError nil end # Allow fields to be set via Hash notation # # method - key name # value - value to set for the attr key # # Returns - value def []=(method, value) send("#{method}=", value) rescue NoMethodError nil end ATTR_SETTER = '='.freeze ATTR_PREDICATE = '?'.freeze # Provides access to a resource's attributes. def method_missing(method, *args) attr_name, suffix = method.to_s.scan(/([a-z0-9\_]+)(\?|\=)?$/i).first if suffix == ATTR_SETTER @_metaclass.send(:attr_accessor, attr_name) @_fields << attr_name.to_sym send(method, args.first) elsif attr_name && @_fields.include?(attr_name.to_sym) value = @attrs[attr_name.to_sym] case suffix when nil @_metaclass.send(:attr_accessor, attr_name) value when ATTR_PREDICATE then !!value end elsif suffix.nil? && SPECIAL_METHODS.include?(attr_name) instance_variable_get "@_#{attr_name}" elsif attr_name && !@_fields.include?(attr_name.to_sym) nil else super end end # Wire up accessor methods to pull from attrs def self.attr_accessor(*attrs) attrs.each do |attribute| class_eval do define_method attribute do @attrs[attribute.to_sym] end define_method "#{attribute}=" do |value| @attrs[attribute.to_sym] = value end define_method "#{attribute}?" do !!@attrs[attribute.to_sym] end end end end def inspect to_attrs.respond_to?(:pretty_inspect) ? to_attrs.pretty_inspect : to_attrs.inspect end def each(&block) @attrs.each(&block) end # private def to_yaml_properties [:@attrs, :@_fields, :@_rels] end def to_attrs hash = self.attrs.clone hash.keys.each do |k| if hash[k].is_a?(Sawyer::Resource) hash[k] = hash[k].to_attrs elsif hash[k].is_a?(Array) && hash[k].all?{|el| el.is_a?(Sawyer::Resource)} hash[k] = hash[k].collect{|el| el.to_attrs} end end hash end alias to_hash to_attrs alias to_h to_attrs def marshal_dump [@attrs, @_fields, @_rels] end def marshal_load(dumped) @attrs, @_fields, @_rels = *dumped.shift(3) @_metaclass = (class << self; self; end) end end end lostisland-sawyer-77a10a5/lib/sawyer/response.rb000066400000000000000000000033511422756032000217730ustar00rootroot00000000000000module Sawyer class Response attr_reader :agent, :status, :headers, :env, :body, :rels # Builds a Response after a completed request. # # agent - The Sawyer::Agent that is managing the API connection. # res - A Faraday::Response. def initialize(agent, res, options = {}) @agent = agent @status = res.status @headers = res.headers @env = res.env @body = res.body @rels = process_rels @started = options[:sawyer_started] @ended = options[:sawyer_ended] end def data @data ||= begin return(body) unless (headers[:content_type] =~ /json|msgpack/) process_data(agent.decode_body(body)) end end # Turns parsed contents from an API response into a Resource or # collection of Resources. # # data - Either an Array or Hash parsed from JSON. # # Returns either a Resource or Array of Resources. def process_data(data) case data when Hash then Resource.new(agent, data) when Array then data.map { |hash| process_data(hash) } when nil then nil else data end end # Finds link relations from 'Link' response header # # Returns an array of Relations def process_rels links = ( @headers["Link"] || "" ).split(', ').map do |link| href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures [name.to_sym, Relation.from_link(@agent, name, :href => href)] end Hash[*links.flatten] end def timing @timing ||= @ended - @started end def time @ended end def inspect %(#<#{self.class}: #{@status} @rels=#{@rels.inspect} @data=#{data.inspect}>) end end end lostisland-sawyer-77a10a5/lib/sawyer/serializer.rb000066400000000000000000000057521422756032000223150ustar00rootroot00000000000000require 'date' require 'time' module Sawyer class Serializer def self.any_json yajl || multi_json || json || begin raise RuntimeError, "Sawyer requires a JSON gem: yajl, multi_json, or json" end end def self.yajl require 'yajl' new(Yajl) rescue LoadError end def self.json require 'json' new(JSON) rescue LoadError end def self.multi_json require 'multi_json' new(MultiJson) rescue LoadError end def self.message_pack require 'msgpack' new(MessagePack, :pack, :unpack) rescue LoadError end # Public: Wraps a serialization format for Sawyer. Nested objects are # prepared for serialization (such as changing Times to ISO 8601 Strings). # Any serialization format that responds to #dump and #load will work. def initialize(format, dump_method_name = nil, load_method_name = nil) @format = format @dump = @format.method(dump_method_name || :dump) @load = @format.method(load_method_name || :load) end # Public: Encodes an Object (usually a Hash or Array of Hashes). # # data - Object to be encoded. # # Returns an encoded String. def encode(data) @dump.call(encode_object(data)) end alias dump encode # Public: Decodes a String into an Object (usually a Hash or Array of # Hashes). # # data - An encoded String. # # Returns a decoded Object. def decode(data) return nil if data.nil? || data.strip.empty? decode_object(@load.call(data)) end alias load decode def encode_object(data) case data when Hash then encode_hash(data) when Array then data.map { |o| encode_object(o) } else data end end def encode_hash(hash) hash.keys.each do |key| case value = hash[key] when Date then hash[key] = value.to_time.utc.xmlschema when Time then hash[key] = value.utc.xmlschema when Hash then hash[key] = encode_hash(value) end end hash end def decode_object(data) case data when Hash then decode_hash(data) when Array then data.map { |o| decode_object(o) } else data end end def decode_hash(hash) hash.keys.each do |key| hash[key.to_sym] = decode_hash_value(key, hash.delete(key)) end hash end def decode_hash_value(key, value) if time_field?(key, value) if value.is_a?(String) begin Time.parse(value) rescue ArgumentError value end elsif value.is_a?(Integer) || value.is_a?(Float) Time.at(value) else value end elsif value.is_a?(Hash) decode_hash(value) elsif value.is_a?(Array) value.map { |o| decode_hash_value(key, o) } else value end end def time_field?(key, value) value && (key =~ /_(at|on)\z/ || key =~ /(\A|_)date\z/) end end end lostisland-sawyer-77a10a5/sawyer.gemspec000066400000000000000000000020701422756032000204040ustar00rootroot00000000000000lib = "sawyer" lib_file = File.expand_path("../lib/#{lib}.rb", __FILE__) File.read(lib_file) =~ /\bVERSION\s*=\s*["'](.+?)["']/ version = $1 Gem::Specification.new do |spec| spec.specification_version = 2 if spec.respond_to? :specification_version= spec.required_rubygems_version = Gem::Requirement.new(">= 1.3.5") if spec.respond_to? :required_rubygems_version= spec.name = lib spec.version = version spec.summary = "Secret User Agent of HTTP" spec.authors = ["Rick Olson", "Wynn Netherland"] spec.email = 'technoweenie@gmail.com' spec.homepage = 'https://github.com/lostisland/sawyer' spec.licenses = ['MIT'] spec.add_dependency 'faraday', '>= 0.17.3', '< 3' spec.add_dependency 'addressable', ['>= 2.3.5'] spec.files = %w(Gemfile LICENSE.md README.md Rakefile) spec.files << "#{lib}.gemspec" spec.files += Dir.glob("lib/**/*.rb") spec.files += Dir.glob("script/*") dev_null = File.exist?('/dev/null') ? '/dev/null' : 'NUL' git_files = `git ls-files -z 2>#{dev_null}` spec.files &= git_files.split("\0") if $?.success? end lostisland-sawyer-77a10a5/script/000077500000000000000000000000001422756032000170325ustar00rootroot00000000000000lostisland-sawyer-77a10a5/script/bootstrap000077500000000000000000000000571422756032000207770ustar00rootroot00000000000000#!/bin/sh set -e bundle install --quiet "$@" lostisland-sawyer-77a10a5/script/console000077500000000000000000000002551422756032000204240ustar00rootroot00000000000000#!/usr/bin/env bash # Usage: script/console # Starts an IRB console with this library loaded. gemspec="$(ls *.gemspec | head -1)" exec bundle exec irb -r "${gemspec%.*}" lostisland-sawyer-77a10a5/script/package000077500000000000000000000002311422756032000203470ustar00rootroot00000000000000#!/usr/bin/env bash # Usage: script/gem # Updates the gemspec and builds a new gem in the pkg directory. mkdir -p pkg gem build *.gemspec mv *.gem pkg lostisland-sawyer-77a10a5/script/release000077500000000000000000000006011422756032000203750ustar00rootroot00000000000000#!/usr/bin/env bash # Usage: script/release # Build the package, tag a commit, push it to origin, and then release the # package publicly. set -e version="$(script/package | grep Version: | awk '{print $2}')" [ -n "$version" ] || exit 1 git commit --allow-empty -a -m "Release $version" git tag "v$version" git push origin git push origin "v$version" gem push pkg/*-${version}.gem lostisland-sawyer-77a10a5/script/test000077500000000000000000000001621422756032000177360ustar00rootroot00000000000000#!/usr/bin/env bash # Usage: script/test # Runs the library's test suite. script/bootstrap bundle exec rake test lostisland-sawyer-77a10a5/test/000077500000000000000000000000001422756032000165055ustar00rootroot00000000000000lostisland-sawyer-77a10a5/test/agent_test.rb000066400000000000000000000133641422756032000211760ustar00rootroot00000000000000require File.expand_path("../helper", __FILE__) require 'faraday/adapter/test' module Sawyer class AgentTest < TestCase class InlineRelsParser def parse(data) links = {} data.keys.select {|k| k[/_url$/] }.each {|k| links[k.to_s.gsub(/_url$/, '')] = data.delete(k) } return data, links end end def setup @stubs = Faraday::Adapter::Test::Stubs.new @agent = Sawyer::Agent.new "http://foo.com/a/" do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test, @stubs end end def test_close @agent.close end def test_accesses_root_relations @stubs.get '/a/' do |env| assert_equal 'foo.com', env[:url].host [200, {'Content-Type' => 'application/json'}, Sawyer::Agent.encode( :_links => { :users => {:href => '/users'}})] end assert_equal 200, @agent.root.status assert_equal '/users', @agent.rels[:users].href assert_equal :get, @agent.rels[:users].method end def test_allows_custom_rel_parsing @stubs.get '/a/' do |env| assert_equal 'foo.com', env[:url].host [200, {'Content-Type' => 'application/json'}, Sawyer::Agent.encode( :url => '/', :users_url => '/users', :repos_url => '/repos')] end agent = Sawyer::Agent.new "http://foo.com/a/" do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test, @stubs end agent.links_parser = InlineRelsParser.new assert_equal 200, agent.root.status assert_equal '/users', agent.rels[:users].href assert_equal :get, agent.rels[:users].method assert_equal '/repos', agent.rels[:repos].href assert_equal :get, agent.rels[:repos].method end def test_saves_root_endpoint @stubs.get '/a/' do |env| [200, {}, '{}'] end assert_kind_of Sawyer::Response, @agent.root refute_equal @agent.root.time, @agent.start.time end def test_starts_a_session @stubs.get '/a/' do |env| assert_equal 'foo.com', env[:url].host [200, {'Content-Type' => 'application/json'}, Sawyer::Agent.encode( :_links => { :users => {:href => '/users'}})] end res = @agent.start assert_equal 200, res.status assert_kind_of Sawyer::Resource, resource = res.data assert_equal '/users', resource.rels[:users].href assert_equal :get, resource.rels[:users].method end def test_requests_with_body_and_options @stubs.post '/a/b/c' do |env| assert_equal '{"a":1}', env[:body] assert_equal 'abc', env[:request_headers]['x-test'] assert_equal 'foo=bar', env[:url].query [200, {}, "{}"] end res = @agent.call :post, 'b/c' , {:a => 1}, :headers => {"X-Test" => "abc"}, :query => {:foo => 'bar'} assert_equal 200, res.status end def test_requests_with_body_and_options_to_get @stubs.get '/a/b/c' do |env| assert_nil env[:body] assert_equal 'abc', env[:request_headers]['x-test'] assert_equal 'foo=bar', env[:url].query [200, {}, "{}"] end res = @agent.call :get, 'b/c' , {:a => 1}, :headers => {"X-Test" => "abc"}, :query => {:foo => 'bar'} assert_equal 200, res.status end def test_encodes_and_decodes_times time = Time.at(Time.now.to_i) data = { :a => 1, :b => true, :c => 'c', :created_at => time, :published_at => nil, :updated_at => "An invalid date", :pub_date => time, :subscribed_at => time.to_i, :lost_at => time.to_f, :first_date => false, :validate => true } data = [data.merge(:foo => [data])] encoded = Sawyer::Agent.encode(data) decoded = Sawyer::Agent.decode(encoded) 2.times do assert_equal 1, decoded.size decoded = decoded.shift assert_equal 1, decoded[:a] assert_equal true, decoded[:b] assert_equal 'c', decoded[:c] assert_equal time, decoded[:created_at], "Did not parse created_at as Time" assert_nil decoded[:published_at] assert_equal "An invalid date", decoded[:updated_at] assert_equal time, decoded[:pub_date], "Did not parse pub_date as Time" assert_equal true, decoded[:validate] assert_equal time, decoded[:subscribed_at], "Did not parse subscribed_at as Time" assert_equal time, decoded[:lost_at], "Did not parse lost_at as Time" assert_equal false, decoded[:first_date], "Parsed first_date" decoded = decoded[:foo] end end def test_does_not_encode_non_json_content_types @stubs.get '/a/' do |env| assert_equal 'foo.com', env[:url].host [200, {'Content-Type' => 'text/plain'}, "This is plain text"] end res = @agent.call :get, '/a/', :headers => {"Accept" => "text/plain"} assert_equal 200, res.status assert_equal "This is plain text", res.data end def test_handle_yaml_dump_and_load return unless supports_yaml? require 'yaml' res = Agent.new 'http://example.com', :a => 1 YAML.load(YAML.dump(res)) end def test_handle_marshal_dump_and_load res = Agent.new 'http://example.com', :a => 1 Marshal.load(Marshal.dump(res)) end def test_blank_response_doesnt_raise @stubs.get "/a/" do |env| assert_equal "foo.com", env[:url].host [200, { "Content-Type" => "application/json" }, " "] end agent = Sawyer::Agent.new "http://foo.com/a/" do |conn| conn.adapter :test, @stubs end assert_equal 200, agent.root.status end end end lostisland-sawyer-77a10a5/test/helper.rb000066400000000000000000000006201422756032000203070ustar00rootroot00000000000000require "minitest/autorun" require File.expand_path('../../lib/sawyer', __FILE__) class Sawyer::TestCase < Minitest::Test def default_test end def supports_yaml? return true if ruby_version < yaml_disabled_version ENV["SAWYER_YAML_ENABLED"] == "1" end def ruby_version Gem::Version.new(RUBY_VERSION) end def yaml_disabled_version Gem::Version.new("2.5.0") end end lostisland-sawyer-77a10a5/test/relation_test.rb000066400000000000000000000126611422756032000217140ustar00rootroot00000000000000require File.expand_path("../helper", __FILE__) module Sawyer class RelationTest < TestCase def test_builds_relation_from_hash hash = {:href => '/users/1', :method => 'post'} rel = Sawyer::Relation.from_link(nil, :self, hash) assert_equal :self, rel.name assert_equal '/users/1', rel.href assert_equal :post, rel.method assert_equal [:post], rel.available_methods.to_a end def test_builds_multiple_rels_from_multiple_methods index = { 'comments' => {:href => '/comments', :method => 'get,post'} } rels = Sawyer::Relation.from_links(nil, index) assert_equal 1, rels.size assert_equal [:comments], rels.keys assert rel = rels[:comments] assert_equal '/comments', rel.href assert_equal :get, rel.method assert_equal [:get, :post], rel.available_methods.to_a assert_kind_of Addressable::Template, rel.href_template end def test_builds_rels_from_hash index = { 'self' => '/users/1' } rels = Sawyer::Relation.from_links(nil, index) assert_equal 1, rels.size assert_equal [:self], rels.keys assert rel = rels[:self] assert_equal :self, rel.name assert_equal '/users/1', rel.href assert_equal :get, rel.method assert_equal [:get], rel.available_methods.to_a assert_kind_of Addressable::Template, rel.href_template end def test_builds_rels_from_hash_index index = { 'self' => {:href => '/users/1'} } rels = Sawyer::Relation.from_links(nil, index) assert_equal 1, rels.size assert_equal [:self], rels.keys assert rel = rels[:self] assert_equal :self, rel.name assert_equal '/users/1', rel.href assert_equal :get, rel.method assert_equal [:get], rel.available_methods.to_a assert_kind_of Addressable::Template, rel.href_template end def test_builds_rels_from_nil rels = Sawyer::Relation.from_links nil, nil assert_equal 0, rels.size assert_equal [], rels.keys end def test_relation_api_calls agent = Sawyer::Agent.new "http://foo.com/a/" do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test do |stubs| stubs.get '/a/1' do |env| assert_equal 'Bar', env.request_headers['Foo'] [200, {}, '{}'] end stubs.delete '/a/1' do |env| assert_equal 'Bar', env.request_headers['Foo'] [204, {}, '{}'] end end end rel = Sawyer::Relation.new agent, :self, "/a/1", "get,put,delete" assert_equal :get, rel.method [:get, :put, :delete].each do |m| assert rel.available_methods.include?(m), "#{m.inspect} is not available: #{rel.available_methods.inspect}" end options = { headers: { 'Foo' => 'Bar' } } assert_equal 200, rel.call(nil, options).status assert_equal 200, rel.call(options.merge(method: :head)).status assert_equal 204, rel.call(nil, options.merge(method: :delete)).status assert_raises ArgumentError do rel.call nil, :method => :post end assert_equal 200, rel.head(options).status assert_equal 200, rel.get(options).status assert_equal 204, rel.delete(nil, options).status assert_raises ArgumentError do rel.post end end def test_relation_api_calls_with_uri_tempate agent = Sawyer::Agent.new "http://foo.com/a" do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test do |stubs| stubs.get '/octocat/hello' do |env| assert_equal "a=1&b=2", env[:url].query [200, {}, '{}'] end stubs.get '/a' do [404, {}, '{}'] end end end rel = Sawyer::Relation.new agent, :repo, "{/user,repo}{?a,b}" assert_equal '', rel.href assert_equal '/octocat', rel.href(:user => :octocat) assert_equal 404, rel.get.status assert_equal 200, rel.get(:uri => {'user' => 'octocat', 'repo' => 'hello', 'a' => 1, 'b' => 2}).status end def test_handles_invalid_uri hash = {:href => '/this has spaces', :method => 'post'} rel = Sawyer::Relation.from_link(nil, :self, hash) assert_equal :self, rel.name assert_equal '/this has spaces', rel.href end def test_allows_all_methods_when_not_in_strict_mode agent = Sawyer::Agent.new "http://foo.com/a/", :allow_undefined_methods => true do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test do |stubs| stubs.get '/a/1' do [200, {}, '{}'] end stubs.delete '/a/1' do [204, {}, '{}'] end stubs.post '/a/1' do [200, {}, '{}'] end stubs.put '/a/1' do [204, {}, '{}'] end end end rel = Sawyer::Relation.new agent, :self, "/a/1" assert_equal 200, rel.get.status assert_equal 200, rel.post.status assert_equal 204, rel.put.status assert_equal 204, rel.delete.status end def test_map_inspect map = Sawyer::Relation::Map.new hash = {:href => '/users/1', :method => 'post'} rel = Sawyer::Relation.from_link(nil, :self, hash) map << rel assert_equal "{:self_url=>\"/users/1\"}", map.inspect.strip end end end lostisland-sawyer-77a10a5/test/resource_test.rb000066400000000000000000000124471422756032000217300ustar00rootroot00000000000000require File.expand_path("../helper", __FILE__) module Sawyer class ResourceTest < TestCase def setup @stubs = Faraday::Adapter::Test::Stubs.new @agent = Sawyer::Agent.new "http://foo.com/a/" do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test, @stubs end end def test_accessible_keys res = Resource.new @agent, :a => 1, :_links => {:self => {:href => '/'}} assert_equal 1, res.a assert res.rels[:self] assert_equal @agent, res.agent assert_equal 1, res.fields.size assert res.fields.include?(:a) end def test_clashing_keys res = Resource.new @agent, :agent => 1, :rels => 2, :fields => 3, :_links => {:self => {:href => '/'}} assert_equal 1, res.agent assert_equal 2, res.rels assert_equal 3, res.fields assert res._rels[:self] assert_equal @agent, res._agent assert_equal 3, res._fields.size [:agent, :rels, :fields].each do |f| assert res._fields.include?(f) end end def test_nested_object res = Resource.new @agent, :user => {:id => 1, :_links => {:self => {:href => '/users/1'}}}, :_links => {:self => {:href => '/'}} assert_equal '/', res.rels[:self].href assert_kind_of Resource, res.user assert_equal 1, res.user.id assert_equal '/users/1', res.user.rels[:self].href end def test_nested_collection res = Resource.new @agent, :users => [{:id => 1, :_links => {:self => {:href => '/users/1'}}}], :_links => {:self => {:href => '/'}} assert_equal '/', res.rels[:self].href assert_kind_of Array, res.users assert user = res.users.first assert_kind_of Resource, user assert_equal 1, user.id assert_equal '/users/1', user.rels[:self].href end def test_attribute_predicates res = Resource.new @agent, :a => 1, :b => true, :c => nil, :d => false assert res.a? assert res.b? assert !res.c? assert !res.d? end def test_attribute_setter res = Resource.new @agent, :a => 1 assert_equal 1, res.a assert !res.key?(:b) res.b = 2 assert_equal 2, res.b assert res.key?(:b) end def test_dynamic_attribute_methods_from_getter res = Resource.new @agent, :a => 1 assert res.key?(:a) assert res.respond_to?(:a) assert res.respond_to?(:a=) assert_equal 1, res.a assert res.respond_to?(:a) assert res.respond_to?(:a=) end def test_nillable_attribute_getters res = Resource.new @agent, :a => 1 assert !res.key?(:b) assert !res.respond_to?(:b) assert !res.respond_to?(:b=) assert_nil res.b res.b end def test_dynamic_attribute_methods_from_setter res = Resource.new @agent, :a => 1 assert !res.key?(:b) assert !res.respond_to?(:b) assert !res.respond_to?(:b=) res.b = 1 assert res.key?(:b) assert res.respond_to?(:b) assert res.respond_to?(:b=) end def test_attrs res = Resource.new @agent, :a => 1 hash = {:a => 1 } assert_equal hash, res.attrs end def test_to_h res = Resource.new @agent, :a => 1 hash = {:a => 1 } assert_equal hash, res.to_h end def test_to_h_with_nesting res = Resource.new @agent, :a => {:b => 1} hash = {:a => {:b => 1}} assert_equal hash, res.to_h end def test_to_attrs_for_sawyer_resource_arrays res = Resource.new @agent, :a => 1, :b => [Resource.new(@agent, :a => 2)] hash = {:a => 1, :b => [{:a => 2}]} assert_equal hash, res.to_attrs end def test_handle_hash_notation_with_string_key res = Resource.new @agent, :a => 1 assert_equal 1, res['a'] res[:b] = 2 assert_equal 2, res.b end def test_simple_rel_parsing @agent.links_parser = Sawyer::LinkParsers::Simple.new res = Resource.new @agent, :url => '/', :user => { :id => 1, :url => '/users/1', :followers_url => '/users/1/followers' } assert_equal '/', res.rels[:self].href assert_kind_of Resource, res.user assert_equal '/', res.url assert_equal 1, res.user.id assert_equal '/users/1', res.user.rels[:self].href assert_equal '/users/1', res.user.url assert_equal '/users/1/followers', res.user.rels[:followers].href assert_equal '/users/1/followers', res.user.followers_url end def test_handle_yaml_dump return unless supports_yaml? require 'yaml' res = Resource.new @agent, :a => 1 YAML.dump(res) end def test_handle_marshal_dump dump = Marshal.dump(Resource.new(@agent, :a => 1)) resource = Marshal.load(dump) assert_equal 1, resource.a end def test_inspect resource = Resource.new @agent, :a => 1 assert_equal "{:a=>1}", resource.inspect.strip end def test_each resource = Resource.new @agent, { :a => 1, :b => 2 } output = [] resource.each { |k,v| output << [k,v] } assert_equal [[:a, 1], [:b, 2]], output end def test_enumerable resource = Resource.new @agent, { :a => 1, :b => 2 } enum = resource.map assert_equal Enumerator, enum.class end end end lostisland-sawyer-77a10a5/test/response_test.rb000066400000000000000000000044711422756032000217350ustar00rootroot00000000000000require File.expand_path("../helper", __FILE__) module Sawyer class ResponseTest < TestCase def setup @now = Time.now @stubs = Faraday::Adapter::Test::Stubs.new @agent = Sawyer::Agent.new "http://foo.com" do |conn| conn.builder.handlers.delete(Faraday::Adapter::NetHttp) conn.adapter :test, @stubs do |stub| stub.get '/' do [200, { 'Content-Type' => 'application/json', 'Link' => '; rel="next", ; rel="last"' }, Sawyer::Agent.encode( :a => 1, :_links => { :self => {:href => '/a', :method => 'POST'} } )] end stub.get '/emails' do emails = %w(rick@example.com technoweenie@example.com) [200, {'Content-Type' => 'application/json'}, Sawyer::Agent.encode(emails)] end end end @res = @agent.start assert_kind_of Sawyer::Response, @res end def test_gets_status assert_equal 200, @res.status end def test_gets_headers assert_equal 'application/json', @res.headers['content-type'] end def test_gets_env assert_equal :get, @res.env.method end def test_gets_raw_body assert_kind_of String, @res.body assert (@res.body =~ /^\{/) end def test_gets_body assert_equal 1, @res.data.a assert_equal [:a], @res.data.fields.to_a end def test_gets_rels assert_equal '/starred?page=2', @res.rels[:next].href assert_equal :get, @res.rels[:next].method assert_equal '/starred?page=19', @res.rels[:last].href assert_equal :get, @res.rels[:next].method assert_equal '/a', @res.data.rels[:self].href assert_equal :post, @res.data.rels[:self].method end def test_gets_response_timing assert @res.timing > 0 assert @res.time >= @now end def test_makes_request_from_relation @stubs.post '/a' do [201, {'Content-Type' => 'application/json'}, ""] end res = @res.data.rels[:self].call assert_equal 201, res.status assert_nil res.data end def test_handles_arrays_of_strings res = @agent.call(:get, '/emails') assert_equal 'rick@example.com', res.data.first end end end