This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
source "http://rubygems.org"
|
||||
|
||||
gemspec
|
||||
|
||||
gem "rake"
|
||||
|
||||
group :test do
|
||||
gem "minitest"
|
||||
end
|
||||
|
||||
install_if -> { ENV["FARADAY_VERSION"] } do
|
||||
gem "faraday", ENV["FARADAY_VERSION"]
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (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.
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,12 @@
|
||||
require '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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
module Sawyer
|
||||
VERSION = "0.9.2"
|
||||
|
||||
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__) }
|
||||
@@ -0,0 +1,163 @@
|
||||
require '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
|
||||
@@ -0,0 +1,15 @@
|
||||
module Sawyer
|
||||
module LinkParsers
|
||||
|
||||
class Hal
|
||||
|
||||
def parse(data)
|
||||
links = data.delete(:_links)
|
||||
|
||||
return data, links
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
module 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
|
||||
@@ -0,0 +1,272 @@
|
||||
module 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
|
||||
@@ -0,0 +1,160 @@
|
||||
require 'forwardable'
|
||||
|
||||
module Sawyer
|
||||
class Resource
|
||||
SPECIAL_METHODS = Set.new(%w(agent rels fields))
|
||||
attr_reader :_agent, :_rels, :_fields
|
||||
attr_reader :attrs
|
||||
include Enumerable
|
||||
extend Forwardable
|
||||
|
||||
# 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
|
||||
|
||||
def_delegators :attrs, :dig, :fetch
|
||||
|
||||
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
|
||||
@@ -0,0 +1,72 @@
|
||||
module 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
|
||||
@@ -0,0 +1,129 @@
|
||||
require '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
|
||||
@@ -0,0 +1,31 @@
|
||||
lib = "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
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
bundle install --quiet "$@"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/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%.*}"
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/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
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/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
|
||||
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage: script/test
|
||||
# Runs the library's test suite.
|
||||
|
||||
script/bootstrap
|
||||
bundle exec rake test
|
||||
Reference in New Issue
Block a user