[alfred] Add Giphy workflow

pull/28/head
Alpha Chen 9 years ago
parent e7f387b720
commit 9e43db5523

@ -67,7 +67,7 @@
<key>78F38D7E-7CF4-41FC-AD5F-C4017FFCA0AE</key> <key>78F38D7E-7CF4-41FC-AD5F-C4017FFCA0AE</key>
<dict> <dict>
<key>enabled</key> <key>enabled</key>
<true/> <false/>
<key>keyword</key> <key>keyword</key>
<string>giphy</string> <string>giphy</string>
<key>plusSpaces</key> <key>plusSpaces</key>

@ -0,0 +1,4 @@
source "https://rubygems.org"
gem "alphred", "~> 1.1"
gem "faraday", "~> 0.9"

@ -0,0 +1,19 @@
GEM
remote: https://rubygems.org/
specs:
alphred (1.1.1)
builder (~> 3.2)
builder (3.2.2)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
multipart-post (2.0.0)
PLATFORMS
ruby
DEPENDENCIES
alphred (~> 1.1)
faraday (~> 0.9)
BUNDLED WITH
1.10.6

@ -0,0 +1,14 @@
# Giphy.alfredworkflow
A workflow for [Alfred][alfred] that searches [Giphy][giphy] for animated gifs.
Actioning on a search item will preview the gif using Quick Look and copy the
direct URL of the gif to the clipboard.
[alfred]: https://www.alfredapp.com/
[giphy]: http://giphy.com/
## TODO
- Get a production Giphy API key?
- Copy the link to the Giphy page (instead of the gif) with a keyboard
modifier.

@ -0,0 +1,120 @@
$LOAD_PATH.unshift(File.expand_path("../vendor/bundle", __FILE__))
require "bundler/setup"
require "json"
require "alphred"
require "faraday"
module Giphy
API_KEY = "dc6zaTOxFJmzC"
class Gif
attr_reader :data
def initialize(data)
@data = data
end
def thumbnail
return @thumbnail if defined?(@thumbnail)
url = self.data["images"]["fixed_width_small_still"]["url"]
@thumbnail = Thumbnail.new(self.name, url)
end
def id
self.data["id"]
end
def size
self.data["images"]["original"]["size"]
end
def name
self.data["url"].split(?/).last.sub(/\-[^-]+$/, "")
end
def urls
Hash[%w[ url mp4 webp ].map {|key| [key, self.data["images"]["original"][key]] }]
end
end
class Thumbnail
attr_reader *%i[ name url ]
def initialize(name, url)
@name, @url = name, url
end
def download!
File.write(self.path, Faraday.get(url).body, mode: ?w)
end
def path
ext = File.extname(self.url)
File.join(self.dir, "#{self.name}#{ext}")
end
def dir
return @dir if defined?(@dir)
dir = File.expand_path(ENV["alfred_workflow_cache"])
Dir.mkdir(dir) unless Dir.exist?(dir)
@dir = dir
end
end
class FileSize
attr_reader :size
def initialize(size)
@size = size.to_i
end
def to_s
"%.1f%s" % case self.size
when (0...1_000)
[self.size, nil]
when (1_000...1_000_000)
[self.size / 1_000.0, "KB"]
else
[self.size / 1_000_000.0, "MB"]
end
end
end
end
if __FILE__ == $0
query = ARGV.shift
resp = Faraday.get("http://api.giphy.com/v1/gifs/search",
{ q: query,
limit: 9,
api_key: Giphy::API_KEY })
data = JSON.load(resp.body)["data"]
gifs = data.map {|gif| Giphy::Gif.new(gif) }
threads = gifs.map do |gif|
Thread.new do
gif.thumbnail.download!
end
end
threads.each(&:join)
items = gifs.map do |gif|
Alphred::Item.new(
title: gif.name,
subtitle: "#{gif.id} - #{Giphy::FileSize.new(gif.size)}",
arg: JSON.dump(gif.urls),
icon: gif.thumbnail.path,
)
end
# items << Alphred::Item.new(
# title: "[Powered By Giphy]",
# icon: "icon.png",
# )
puts Alphred::Items.new(*items).to_xml
end

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>bundleid</key>
<string>com.arbitrarydefinitions.giphy</string>
<key>category</key>
<string>Internet</string>
<key>connections</key>
<dict>
<key>0744A6DC-F5C1-4D81-8056-8C281A5494D2</key>
<array>
<dict>
<key>destinationuid</key>
<string>FF2DAFF4-2EF6-427C-A48F-A71F47A52E8D</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
</dict>
<dict>
<key>destinationuid</key>
<string>2C624EB9-DB8E-48FA-A0E7-152BB7F7817A</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
</dict>
</array>
<key>2C624EB9-DB8E-48FA-A0E7-152BB7F7817A</key>
<array>
<dict>
<key>destinationuid</key>
<string>52E6556C-14D2-4D85-8BDD-4B13B2B5943D</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
</dict>
</array>
<key>FF2DAFF4-2EF6-427C-A48F-A71F47A52E8D</key>
<array>
<dict>
<key>destinationuid</key>
<string>21CA0C10-1AC1-44DA-A415-E53C081F77AE</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
</dict>
</array>
</dict>
<key>createdby</key>
<string>Alpha Chen</string>
<key>description</key>
<string>Search Giphy for animated gifs</string>
<key>disabled</key>
<false/>
<key>name</key>
<string>Giphy</string>
<key>objects</key>
<array>
<dict>
<key>config</key>
<dict>
<key>concurrently</key>
<false/>
<key>escaping</key>
<integer>102</integer>
<key>script</key>
<string>require "json"
print JSON.load("{query}")["url"]</string>
<key>type</key>
<integer>2</integer>
</dict>
<key>type</key>
<string>alfred.workflow.action.script</string>
<key>uid</key>
<string>FF2DAFF4-2EF6-427C-A48F-A71F47A52E8D</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>concurrently</key>
<false/>
<key>escaping</key>
<integer>102</integer>
<key>script</key>
<string>curl "{query}" --silent --output "$alfred_workflow_cache/giphy.gif" &amp;&amp; qlmanage -p "$alfred_workflow_cache/giphy.gif" &gt;&amp; /dev/null</string>
<key>type</key>
<integer>0</integer>
</dict>
<key>type</key>
<string>alfred.workflow.output.script</string>
<key>uid</key>
<string>21CA0C10-1AC1-44DA-A415-E53C081F77AE</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>argumenttype</key>
<integer>0</integer>
<key>escaping</key>
<integer>102</integer>
<key>keyword</key>
<string>giphy</string>
<key>queuedelaycustom</key>
<integer>3</integer>
<key>queuedelayimmediatelyinitially</key>
<false/>
<key>queuedelaymode</key>
<integer>1</integer>
<key>queuemode</key>
<integer>2</integer>
<key>runningsubtext</key>
<string>Searching Giphy...</string>
<key>script</key>
<string>ruby giphy.rb "{query}"</string>
<key>title</key>
<string>Search Giphy</string>
<key>type</key>
<integer>0</integer>
<key>withspace</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.input.scriptfilter</string>
<key>uid</key>
<string>0744A6DC-F5C1-4D81-8056-8C281A5494D2</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>autopaste</key>
<false/>
<key>clipboardtext</key>
<string>{query}</string>
</dict>
<key>type</key>
<string>alfred.workflow.output.clipboard</string>
<key>uid</key>
<string>52E6556C-14D2-4D85-8BDD-4B13B2B5943D</string>
<key>version</key>
<integer>0</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>concurrently</key>
<false/>
<key>escaping</key>
<integer>102</integer>
<key>script</key>
<string>require "json"
print JSON.load("{query}")["gif"]</string>
<key>type</key>
<integer>2</integer>
</dict>
<key>type</key>
<string>alfred.workflow.action.script</string>
<key>uid</key>
<string>2C624EB9-DB8E-48FA-A0E7-152BB7F7817A</string>
<key>version</key>
<integer>0</integer>
</dict>
</array>
<key>readme</key>
<string></string>
<key>uidata</key>
<dict>
<key>0744A6DC-F5C1-4D81-8056-8C281A5494D2</key>
<dict>
<key>ypos</key>
<real>70</real>
</dict>
<key>21CA0C10-1AC1-44DA-A415-E53C081F77AE</key>
<dict>
<key>ypos</key>
<real>10</real>
</dict>
<key>2C624EB9-DB8E-48FA-A0E7-152BB7F7817A</key>
<dict>
<key>ypos</key>
<real>130</real>
</dict>
<key>52E6556C-14D2-4D85-8BDD-4B13B2B5943D</key>
<dict>
<key>ypos</key>
<real>130</real>
</dict>
<key>FF2DAFF4-2EF6-427C-A48F-A71F47A52E8D</key>
<dict>
<key>ypos</key>
<real>10</real>
</dict>
</dict>
<key>webaddress</key>
<string>https://github.com/kejadlen/giphy.alfredworkflow</string>
</dict>
</plist>

@ -0,0 +1,9 @@
require 'rbconfig'
# ruby 1.8.7 doesn't define RUBY_ENGINE
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
ruby_version = RbConfig::CONFIG["ruby_version"]
path = File.expand_path('..', __FILE__)
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/builder-3.2.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/alphred-1.1.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/multipart-post-2.0.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/faraday-0.9.2/lib"

@ -0,0 +1,9 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

@ -0,0 +1,14 @@
## Unreleased
## [1.1.1] - 2015-11-05
### Changed
- Don't crash when the config file doesn't exist.
## [1.1.0] - 2015-11-01
### Added
- `Config` class for handling persistent configuration across workflow updates.
- CHANGELOG
## [1.0.0] - 2015-10-31
### Added
- Library for making Alfred workflows.

@ -0,0 +1,13 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)

@ -0,0 +1,10 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in alphred.gemspec
gemspec
group :development do
gem "guard"
gem "guard-minitest"
gem "pry"
end

@ -0,0 +1,5 @@
guard :minitest do
watch(%r{^test/(.*)\/?test_(.*)\.rb$})
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
watch(%r{^test/test_helper\.rb$}) { 'test' }
end

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Alpha Chen
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,152 @@
# Alphred
Alphred is a library for making Alfred workflows in Ruby. It's designed
specifically for how I work, so assumes that you manage dependencies with
[Bundler][bundler] and Rubies with [chruby][chruby].
[bundler]: http://bundler.io/
[chruby]: https://github.com/postmodern/chruby
## Usage
The [example script filter][scriptfilter] would look like this using Alphred:
[scriptfilter]: https://www.alfredapp.com/help/workflows/inputs/script-filter/
``` ruby
items = Alphred::Items.new(
Alphred::Item.new(uid: "desktop",
arg: "~/Desktop",
valid: true,
autocomplete: "Desktop",
type: :file,
title: "Desktop",
subtitle: "~/Desktop",
icon: { value: "~/Desktop", type: :fileicon }),
Alphred::Item.new(uid: "flickr",
valid: false,
autocomplete: "flickr",
title: "Flickr",
icon: "flickr.png"),
Alphred::Item.new(uid: "image",
autocomplete: "My holiday photo",
type: :file,
title: "My holiday photo",
subtitle: "~/Pictures/My holiday photo.jpg",
icon: { value: "public.jpeg", type: :filetype }),
Alphred::Item.new(uid: "home",
arg: "~/",
valid: true,
autocomplete: "Home",
type: :file,
title: "Home Folder",
subtitle: "Home folder ~/",
icon: { value: "~/", type: :fileicon },
mods: { shift: "Subtext when shift is pressed",
fn: "Subtext when fn is pressed",
ctrl: "Subtext when ctrl is pressed",
alt: "Subtext when alt is pressed",
cmd: "Subtext when cmd is pressed" },
text: { copy: "Text when copying",
largetype: "Text for LargeType" }))
items.to_xml
```
This produces the following XML:
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item uid="desktop" arg="~/Desktop" autocomplete="Desktop" type="file" valid="yes">
<title>Desktop</title>
<subtitle>~/Desktop</subtitle>
<icon type="fileicon">~/Desktop</icon>
</item>
<item uid="flickr" autocomplete="flickr" valid="no">
<title>Flickr</title>
<icon>flickr.png</icon>
</item>
<item uid="image" autocomplete="My holiday photo" type="file">
<title>My holiday photo</title>
<subtitle>~/Pictures/My holiday photo.jpg</subtitle>
<icon type="filetype">public.jpeg</icon>
</item>
<item uid="home" arg="~/" autocomplete="Home" type="file" valid="yes">
<title>Home Folder</title>
<subtitle>Home folder ~/</subtitle>
<icon type="fileicon">~/</icon>
<subtitle mod="shift">Subtext when shift is pressed</subtitle>
<subtitle mod="fn">Subtext when fn is pressed</subtitle>
<subtitle mod="ctrl">Subtext when ctrl is pressed</subtitle>
<subtitle mod="alt">Subtext when alt is pressed</subtitle>
<subtitle mod="cmd">Subtext when cmd is pressed</subtitle>
<text type="copy">Text when copying</text>
<text type="largetype">Text for LargeType</text>
</item>
</items>
```
### Workflow Configuration
`Alphred::Config` provides some helpers for managing configuration that should
persist when updating the workflow. This configuration is stored in an JSON
file in the workflow data directory.
``` ruby
# config.rb
module Workflow
defaults = { foo: "bar" }
Config = Alphred::Config.new(**defaults)
```
The corresponding Script Filter input and Run Action output then look like this:
``` shell
# script filter
ruby -r./config -e'puts Workflow::Config.filter_xml("{query}")'
```
``` shell
# run action
ruby -r./config -e'Forecast::Config.update!("{query}")'
```
### Releasing
Including `alphred/tasks` in your `Rakefile` will allow access to Alphred's
Rake tasks for releasing a workflow. `release` will tag the current commit with
the provided version and create a .alfredworkflow package with vendored gem
dependencies.
## TODO
- Add development mode for easier debugging. (Nicer errors, etc.)
- Rake task for installing to Alfred
## Development
After checking out the repo, run `bundle install` to install dependencies.
Then, run `rake test` to run the tests. You can also run `rake console` for an
interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To
release a new version, update the version number in `version.rb`, and then run
`bundle exec rake 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
Bug reports and pull requests are welcome on GitHub at
https://github.com/kejadlen/alphred. This project is intended to be a safe,
welcoming space for collaboration, and contributors are expected to adhere to
the [Contributor Covenant](contributor-covenant.org) code of conduct.
## License
The gem is available as open source under the terms of the [MIT
License](http://opensource.org/licenses/MIT).

@ -0,0 +1,17 @@
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "lib"
t.test_files = FileList['test/**/test_*.rb']
end
task :console do
require "bundler/setup"
require "alphred"
require "pry"
Pry.start
end
task :default => :test

@ -0,0 +1,26 @@
# coding: utf-8
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "alphred/version"
Gem::Specification.new do |spec|
spec.name = "alphred"
spec.version = Alphred::VERSION
spec.authors = ["Alpha Chen"]
spec.email = ["alpha.chen@gmail.com"]
spec.summary = %q{Helper utilities for making Alfred workflows.}
spec.homepage = "https://github.com/kejadlen/alph"
spec.license = "MIT"
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.bindir = "bin"
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency "builder", "~> 3.2"
spec.add_development_dependency "bundler", "~> 1.10"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "minitest"
end

@ -0,0 +1,10 @@
require "builder"
require_relative "alphred/version"
require_relative "alphred/config"
require_relative "alphred/icon"
require_relative "alphred/item"
require_relative "alphred/items"
require_relative "alphred/mods"
require_relative "alphred/text"

@ -0,0 +1,55 @@
require "json"
module Alphred
class Config
def self.load(**defaults)
config = self.new(**defaults)
config.load!
config
end
attr_reader :data
def initialize(**defaults)
@data = Hash[defaults.map {|k,v| [k.to_s, v.to_s] }]
end
def load!
return unless File.exist?(self.path)
self.data.merge!(JSON.load(File.open(self.path)))
end
def update!(json)
data = self.data.merge(JSON.load(json))
File.write(self.path, JSON.dump(data), mode: ?w)
end
def [](key)
self.data[key.to_s]
end
def filter_xml(filter=nil)
filter ||= ""
items = self.data.map do |key, value|
title = if filter.empty?
"Unset #{key}"
else
"Set #{key} to #{filter}"
end
Item.new(
uid: key,
arg: JSON.dump(key => filter),
title: title,
subtitle: self[key]
)
end
Items.new(*items).to_xml
end
def path
File.join(ENV["alfred_workflow_data"], 'config.json')
end
end
end

@ -0,0 +1,39 @@
require "builder"
module Alphred
class Icon
VALID_TYPES = %i[ fileicon filetype ]
attr_accessor *%i[ value type ]
def initialize(**kwargs)
raise ArgumentError.new("missing keyword: value") unless kwargs.has_key?(:value)
@value = kwargs[:value]
self.type = kwargs[:type] if kwargs.has_key?(:type)
end
def type=(type)
raise ArgumentError.new("`type` must be one of #{VALID_TYPES}") unless type.nil? || VALID_TYPES.include?(type)
@type = type
end
def to_xml(xml=nil)
xml ||= Builder::XmlMarkup.new(indent: 2)
attrs = {}
attrs[:type] = self.type unless self.type.nil?
xml.icon self.value, attrs
end
end
end
module Kernel
def Icon(value)
case value
when Alphred::Icon then value
when String then Alphred::Icon.new(value: value)
when Hash then Alphred::Icon.new(**value)
end
end
end

@ -0,0 +1,59 @@
require "builder"
require_relative "mods"
require_relative "text"
module Alphred
class Item
VALID_TYPES = %i[ default file file_skipcheck ]
attr_accessor *%i[ uid arg valid autocomplete title subtitle mods icon text ]
def initialize(**kwargs)
raise ArgumentError.new("missing keyword: title") unless kwargs.has_key?(:title)
@title = kwargs[:title]
%i[ uid arg valid autocomplete subtitle ].each do |attr|
self.instance_variable_set("@#{attr}", kwargs[attr]) if kwargs.has_key?(attr)
end
@icon = Icon(kwargs[:icon]) if kwargs.has_key?(:icon)
@text = Text.new(kwargs[:text]) if kwargs.has_key?(:text)
@mods = Mods.new(kwargs[:mods]) if kwargs.has_key?(:mods)
self.type = kwargs[:type] if kwargs.has_key?(:type)
end
def type=(type)
raise ArgumentError.new("`type` must be one of #{VALID_TYPES}") unless type.nil? || VALID_TYPES.include?(type)
@type = type
end
def type
@type && @type.to_s.gsub(?_, ?:)
end
def to_xml(xml=nil)
xml ||= Builder::XmlMarkup.new(indent: 2)
xml.item self.attrs do
xml.title self.title
xml.subtitle self.subtitle unless self.subtitle.nil?
self.icon.to_xml(xml) unless self.icon.nil?
self.mods.to_xml(xml) unless self.mods.nil?
self.text.to_xml(xml) unless self.text.nil?
end
end
def attrs
attrs = {}
%i[ uid arg autocomplete type ].each do |attr|
value = self.send(attr)
attrs[attr] = value unless value.nil?
end
attrs[:valid] = (self.valid) ? "yes" : "no" unless self.valid.nil?
attrs
end
end
end

@ -0,0 +1,23 @@
require "builder"
require "delegate"
module Alphred
class Items < DelegateClass(Array)
attr_reader :items
def initialize(*items)
@items = items
super(@items)
end
def to_xml
xml = Builder::XmlMarkup.new(indent: 2)
xml.instruct! :xml
xml.items do
self.items.each do |item|
item.to_xml(xml)
end
end
end
end
end

@ -0,0 +1,22 @@
require "builder"
module Alphred
class Mods
MODS = %i[ shift fn ctrl alt cmd ]
attr_accessor *MODS
def initialize(**kwargs)
MODS.each do |mod|
self.instance_variable_set("@#{mod}", kwargs[mod]) if kwargs.has_key?(mod)
end
end
def to_xml(xml)
MODS.each do |mod|
value = self.send(mod)
xml.subtitle value, mod: mod unless value.nil?
end
end
end
end

@ -0,0 +1,40 @@
require "rake"
namespace :alphred do
desc "Prepare a release, named after the directory"
task :release, [:version] => [:tag, :package]
desc "Tag the current commit in git with VERSION"
task :tag, [:version] do |t, args|
version = args[:version]
git_status = `git status --porcelain`
fail <<-FAIL unless git_status.empty?
Can't tag #{version}: dirty working directory.
FAIL
sh "git tag #{version}"
end
desc "Create an alfredworkflow package with vendored dependencies"
task :package do
restore_bundler_config do
cmd = "bundle install --standalone --path vendor/bundle --without development test"
sh "chruby-exec 2.0.0 -- #{cmd}"
end
sh "zip -r #{application_dir.pathmap("%n.alfredworkflow")} *"
rm_rf "vendor"
end
def application_dir
Rake.application.original_dir
end
def restore_bundler_config
path = File.join(application_dir, ".bundle", "config")
config = File.read(path)
yield
ensure
File.write(path, config, mode: ?w)
end
end

@ -0,0 +1,17 @@
require "builder"
module Alphred
class Text
attr_accessor *%i[ copy largetype ]
def initialize(copy: nil, largetype: nil)
@copy = copy
@largetype = largetype
end
def to_xml(xml)
xml.text copy, type: :copy unless self.copy.nil?
xml.text largetype, type: :largetype unless self.largetype.nil?
end
end
end

@ -0,0 +1,107 @@
= Change Log
== Version 3.2.0
* Ruby 2.0 compatibility changes.
* Allow single quoted attributes.
== Version 3.1.0
* Included the to_xs arity patch needed for weird Rails compatibility
issue.
* Escaping newlines in attributes now.
* Allow method caching
== Version 3.0.0
* Ruby 1.9 compatiblity issues.
== Version 2.2.0
* Applied patch from Thijs van der Vossen to allow UTF-8 encoded
output when the encoding is UTF-8 and $KCODE is UTF8.
== Version 2.1.2
* Fixed bug where private methods in kernel could leak through using
tag!(). Thanks to Hagen Overdick for finding and diagnosing this
bug.
== Version 2.1.1
* Fixed typo in XmlMarkup class docs (ident => indent). (from Martin
Fowler).
* Removed extra directory indirection from legacy CVS to SVN move.
* Removed some extraneous tabs from source.
* Fixed test on private methods in blankslate to differentiate between
targetted and untargetted private methods.
* Removed legacy capture of @self in XmlBase (@self was used back when
we used instance eval).
* Added additional tests for global functions (both direct and included).
== Version 2.1.0
* Fixed bug in BlankSlate where including a module into Object could
cause methods to leak into BlankSlate.
* Made BlankSlate available as its own gem. Currently the builder gem
still directly includes the BlankSlate code.
* Added reveal capability to BlankSlate.
== Version 2.0.0
* Added doc directory
* Added unit tests for XmlEvents.
* Added XChar module and used it in the _escape method.
* Attributes are now quoted by default when strings. Use Symbol
attribute values for unquoted behavior.
== Version 1.2.4
* Added a cdata! command to an XML Builder (from Josh Knowles).
== Version 1.2.3
The attributes in the <?xml ... ?> instruction will be ordered:
version, encoding, standalone.
== Version 1.2.2
Another fix for BlankSlate. The Kernal/Object traps added in 1.2.1
failed when a method was defined late more than once. Since the
method was already marked as removed, another attempt to undefine it
raised an error. The fix was to check the list of instance methods
before attempting the undef operation. Thanks to Florian Gross and
David Heinemeier Hansson for the patch.
== Version 1.2.1
BlankSlate now traps method definitions in Kernel and Object to avoid
late method definitions inadvertently becoming part of the definition
of BlankSlate as well.
== Version 1.2.0
Improved support for entity declarations by allowing nested
declarations and removal of the attribute processing.
Added namespace support.
== Version 1.1.0
Added support for comments, entity declarations and processing instructions.
== Version 1.0.0
Removed use of <tt>instace_eval</tt> making the use of XmlMarkup much
less prone to error.
== Version 0.1.1
Bug fix.
== Version 0.1.0
Initial version release.

@ -0,0 +1,20 @@
Copyright (c) 2003-2012 Jim Weirich (jim.weirich@gmail.com)
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,258 @@
# Project: Builder
## Goal
Provide a simple way to create XML markup and data structures.
## Classes
Builder::XmlMarkup:: Generate XML markup notation
Builder::XmlEvents:: Generate XML events (i.e. SAX-like)
**Notes:**
* An <tt>Builder::XmlTree</tt> class to generate XML tree
(i.e. DOM-like) structures is also planned, but not yet implemented.
Also, the events builder is currently lagging the markup builder in
features.
## Usage
```ruby
require 'rubygems'
require_gem 'builder', '~> 2.0'
builder = Builder::XmlMarkup.new
` xml = builder.person { |b| b.name("Jim"); b.phone("555-1234") }
xml #=> <person><name>Jim</name><phone>555-1234</phone></person>
```
or
```ruby
require 'rubygems'
require_gem 'builder'
builder = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
builder.person { |b| b.name("Jim"); b.phone("555-1234") }
#
# Prints:
# <person>
# <name>Jim</name>
# <phone>555-1234</phone>
# </person>
```
## Compatibility
### Version 2.0.0 Compatibility Changes
Version 2.0.0 introduces automatically escaped attribute values for
the first time. Versions prior to 2.0.0 did not insert escape
characters into attribute values in the XML markup. This allowed
attribute values to explicitly reference entities, which was
occasionally used by a small number of developers. Since strings
could always be explicitly escaped by hand, this was not a major
restriction in functionality.
However, it did surprise most users of builder. Since the body text is
normally escaped, everybody expected the attribute values to be
escaped as well. Escaped attribute values were the number one support
request on the 1.x Builder series.
Starting with Builder version 2.0.0, all attribute values expressed as
strings will be processed and the appropriate characters will be
escaped (e.g. "&" will be translated to "&amp;"). Attribute values
that are expressed as Symbol values will not be processed for escaped
characters and will be unchanged in output. (Yes, this probably counts
as Symbol abuse, but the convention is convenient and flexible).
Example:
```ruby
xml = Builder::XmlMarkup.new
xml.sample(:escaped=>"This&That", :unescaped=>:"Here&amp;There")
xml.target! =>
<sample escaped="This&amp;That" unescaped="Here&amp;There"/>
```
### Version 1.0.0 Compatibility Changes
Version 1.0.0 introduces some changes that are not backwards
compatible with earlier releases of builder. The main areas of
incompatibility are:
* Keyword based arguments to +new+ (rather than positional based). It
was found that a developer would often like to specify indentation
without providing an explicit target, or specify a target without
indentation. Keyword based arguments handle this situation nicely.
* Builder must now be an explicit target for markup tags. Instead of
writing
```ruby
xml_markup = Builder::XmlMarkup.new
xml_markup.div { strong("text") }
```
you need to write
```ruby
xml_markup = Builder::XmlMarkup.new
xml_markup.div { xml_markup.strong("text") }
```
* The builder object is passed as a parameter to all nested markup
blocks. This allows you to create a short alias for the builder
object that can be used within the block. For example, the previous
example can be written as:
```ruby
xml_markup = Builder::XmlMarkup.new
xml_markup.div { |xml| xml.strong("text") }
```
* If you have both a pre-1.0 and a post-1.0 gem of builder installed,
you can choose which version to use through the RubyGems
+require_gem+ facility.
```ruby
require_gem 'builder', "~> 0.0" # Gets the old version
require_gem 'builder', "~> 1.0" # Gets the new version
```
## Features
* XML Comments are supported ...
```ruby
xml_markup.comment! "This is a comment"
#=> <!-- This is a comment -->
```
* XML processing instructions are supported ...
```ruby
xml_markup.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
#=> <?xml version="1.0" encoding="UTF-8"?>
```
If the processing instruction is omitted, it defaults to "xml".
When the processing instruction is "xml", the defaults attributes
are:
<b>version</b>: 1.0
<b>encoding</b>: "UTF-8"
(NOTE: if the encoding is set to "UTF-8" and $KCODE is set to
"UTF8", then Builder will emit UTF-8 encoded strings rather than
encoding non-ASCII characters as entities.)
* XML entity declarations are now supported to a small degree.
```ruby
xml_markup.declare! :DOCTYPE, :chapter, :SYSTEM, "../dtds/chapter.dtd"
#=> <!DOCTYPE chapter SYSTEM "../dtds/chapter.dtd">
```
The parameters to a declare! method must be either symbols or
strings. Symbols are inserted without quotes, and strings are
inserted with double quotes. Attribute-like arguments in hashes are
not allowed.
If you need to have an argument to declare! be inserted without
quotes, but the argument does not conform to the typical Ruby
syntax for symbols, then use the :"string" form to specify a symbol.
For example:
```ruby
xml_markup.declare! :ELEMENT, :chapter, :"(title,para+)"
#=> <!ELEMENT chapter (title,para+)>
```
Nested entity declarations are allowed. For example:
```ruby
@xml_markup.declare! :DOCTYPE, :chapter do |x|
x.declare! :ELEMENT, :chapter, :"(title,para+)"
x.declare! :ELEMENT, :title, :"(#PCDATA)"
x.declare! :ELEMENT, :para, :"(#PCDATA)"
end
#=>
<!DOCTYPE chapter [
<!ELEMENT chapter (title,para+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT para (#PCDATA)>
]>
```
* Some support for XML namespaces is now available. If the first
argument to a tag call is a symbol, it will be joined to the tag to
produce a namespace:tag combination. It is easier to show this than
describe it.
```ruby
xml.SOAP :Envelope do ... end
```
Just put a space before the colon in a namespace to produce the
right form for builder (e.g. "<tt>SOAP:Envelope</tt>" =>
"<tt>xml.SOAP :Envelope</tt>")
* String attribute values are <em>now</em> escaped by default by
Builder (<b>NOTE:</b> this is _new_ behavior as of version 2.0).
However, occasionally you need to use entities in attribute values.
Using a symbol (rather than a string) for an attribute value will
cause Builder to not run its quoting/escaping algorithm on that
particular value.
(<b>Note:</b> The +escape_attrs+ option for builder is now
obsolete).
Example:
```ruby
xml = Builder::XmlMarkup.new
xml.sample(:escaped=>"This&That", :unescaped=>:"Here&amp;There")
xml.target! =>
<sample escaped="This&amp;That" unescaped="Here&amp;There"/>
```
* UTF-8 Support
Builder correctly translates UTF-8 characters into valid XML. (New
in version 2.0.0). Thanks to Sam Ruby for the translation code.
You can get UTF-8 encoded output by making sure that the XML
encoding is set to "UTF-8" and that the $KCODE variable is set to
"UTF8".
```ruby
$KCODE = 'UTF8'
xml = Builder::Markup.new
xml.instruct!(:xml, :encoding => "UTF-8")
xml.sample("Iñtërnâtiônàl")
xml.target! =>
"<sample>Iñtërnâtiônàl</sample>"
```
## Links
| Description | Link |
| :----: | :----: |
| Documents | http://builder.rubyforge.org/ |
| Github Clone | git://github.com/jimweirich/builder.git |
| Issue / Bug Reports | https://github.com/jimweirich/builder/issues?state=open |
## Contact
| Description | Value |
| :----: | :----: |
| Author | Jim Weirich |
| Email | jim.weirich@gmail.com |
| Home Page | http://onestepback.org |
| License | MIT Licence (http://www.opensource.org/licenses/mit-license.html) |

@ -0,0 +1,195 @@
# Rakefile for rake -*- ruby -*-
# Copyright 2004, 2005, 2006 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
require 'rake/clean'
require 'rake/testtask'
begin
require 'rubygems'
require 'rubygems/package_task'
require 'rdoc/task'
rescue Exception
nil
end
require './lib/builder/version'
# Determine the current version of the software
CLOBBER.include('pkg', 'html')
CLEAN.include('pkg/builder-*').include('pkg/blankslate-*').exclude('pkg/*.gem')
PKG_VERSION = Builder::VERSION
SRC_RB = FileList['lib/**/*.rb']
# The default task is run if rake is given no explicit arguments.
desc "Default Task"
task :default => :test_all
# Test Tasks ---------------------------------------------------------
desc "Run all tests"
task :test_all => [:test_units]
task :ta => [:test_all]
task :tu => [:test_units]
Rake::TestTask.new("test_units") do |t|
t.test_files = FileList['test/test*.rb']
t.libs << "."
t.verbose = false
end
# Create a task to build the RDOC documentation tree.
if defined?(RDoc)
rd = RDoc::Task.new("rdoc") { |rdoc|
rdoc.rdoc_dir = 'html'
rdoc.title = "Builder for Markup"
rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
rdoc.rdoc_files.include('lib/**/*.rb', '[A-Z]*', 'doc/**/*.rdoc').exclude("TAGS")
rdoc.template = 'doc/jamis.rb'
}
else
rd = Struct.new(:rdoc_files).new([])
end
# ====================================================================
# Create a task that will package the Rake software into distributable
# gem files.
PKG_FILES = FileList[
'[A-Z]*',
'doc/**/*',
'lib/**/*.rb',
'test/**/*.rb',
'rakelib/**/*'
]
PKG_FILES.exclude('test/test_cssbuilder.rb')
PKG_FILES.exclude('lib/builder/css.rb')
PKG_FILES.exclude('TAGS')
BLANKSLATE_FILES = FileList[
'lib/blankslate.rb',
'test/test_blankslate.rb'
]
if ! defined?(Gem)
puts "Package Target requires RubyGEMs"
else
spec = Gem::Specification.new do |s|
#### Basic information.
s.name = 'builder'
s.version = PKG_VERSION
s.summary = "Builders for MarkUp."
s.description = %{\
Builder provides a number of builder objects that make creating structured data
simple to do. Currently the following builder objects are supported:
* XML Markup
* XML Events
}
s.files = PKG_FILES.to_a
s.require_path = 'lib'
s.test_files = PKG_FILES.select { |fn| fn =~ /^test\/test/ }
s.has_rdoc = true
s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
s.rdoc_options <<
'--title' << 'Builder -- Easy XML Building' <<
'--main' << 'README.rdoc' <<
'--line-numbers'
s.author = "Jim Weirich"
s.email = "jim.weirich@gmail.com"
s.homepage = "http://onestepback.org"
s.license = 'MIT'
end
blankslate_spec = Gem::Specification.new do |s|
#### Basic information.
s.name = 'blankslate'
s.version = PKG_VERSION
s.summary = "Blank Slate base class."
s.description = %{\
BlankSlate provides a base class where almost all of the methods from Object and
Kernel have been removed. This is useful when providing proxy object and other
classes that make heavy use of method_missing.
}
s.files = BLANKSLATE_FILES.to_a
s.require_path = 'lib'
s.test_files = PKG_FILES.select { |fn| fn =~ /^test\/test/ }
s.has_rdoc = true
s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
s.rdoc_options <<
'--title' << 'BlankSlate -- Base Class for building proxies.' <<
'--main' << 'README.rdoc' <<
'--line-numbers'
s.author = "Jim Weirich"
s.email = "jim.weirich@gmail.com"
s.homepage = "http://onestepback.org"
s.license = 'MIT'
end
namespace 'builder' do
Gem::PackageTask.new(spec) do |t|
t.need_tar = false
end
end
namespace 'blankslate' do
Gem::PackageTask.new(blankslate_spec) do |t|
t.need_tar = false
end
end
task :package => [:remove_tags, 'builder:package', 'blankslate:package']
end
task :remove_tags do
rm "TAGS" rescue nil
end
# RCov ---------------------------------------------------------------
begin
require 'rcov/rcovtask'
Rcov::RcovTask.new do |t|
t.libs << "test"
t.rcov_opts = [
'-xRakefile', '--text-report'
]
t.test_files = FileList[
'test/test*.rb'
]
t.output_dir = 'coverage'
t.verbose = true
end
rescue LoadError
# No rcov available
end
desc "Install the jamis RDoc template"
task :install_jamis_template do
require 'rbconfig'
dest_dir = File.join(Config::CONFIG['rubylibdir'], "rdoc/generators/template/html")
fail "Unabled to write to #{dest_dir}" unless File.writable?(dest_dir)
install "doc/jamis.rb", dest_dir, :verbose => true
end

@ -0,0 +1,591 @@
module RDoc
module Page
FONTS = "\"Bitstream Vera Sans\", Verdana, Arial, Helvetica, sans-serif"
STYLE = <<CSS
a {
color: #00F;
text-decoration: none;
}
a:hover {
color: #77F;
text-decoration: underline;
}
body, td, p {
font-family: %fonts%;
background: #FFF;
color: #000;
margin: 0px;
font-size: small;
}
#content {
margin: 2em;
}
#description p {
margin-bottom: 0.5em;
}
.sectiontitle {
margin-top: 1em;
margin-bottom: 1em;
padding: 0.5em;
padding-left: 2em;
background: #005;
color: #FFF;
font-weight: bold;
border: 1px dotted black;
}
.attr-rw {
padding-left: 1em;
padding-right: 1em;
text-align: center;
color: #055;
}
.attr-name {
font-weight: bold;
}
.attr-desc {
}
.attr-value {
font-family: monospace;
}
.file-title-prefix {
font-size: large;
}
.file-title {
font-size: large;
font-weight: bold;
background: #005;
color: #FFF;
}
.banner {
background: #005;
color: #FFF;
border: 1px solid black;
padding: 1em;
}
.banner td {
background: transparent;
color: #FFF;
}
h1 a, h2 a, .sectiontitle a, .banner a {
color: #FF0;
}
h1 a:hover, h2 a:hover, .sectiontitle a:hover, .banner a:hover {
color: #FF7;
}
.dyn-source {
display: none;
background: #FFE;
color: #000;
border: 1px dotted black;
margin: 0.5em 2em 0.5em 2em;
padding: 0.5em;
}
.dyn-source .cmt {
color: #00F;
font-style: italic;
}
.dyn-source .kw {
color: #070;
font-weight: bold;
}
.method {
margin-left: 1em;
margin-right: 1em;
margin-bottom: 1em;
}
.description pre {
padding: 0.5em;
border: 1px dotted black;
background: #FFE;
}
.method .title {
font-family: monospace;
font-size: large;
border-bottom: 1px dashed black;
margin-bottom: 0.3em;
padding-bottom: 0.1em;
}
.method .description, .method .sourcecode {
margin-left: 1em;
}
.description p, .sourcecode p {
margin-bottom: 0.5em;
}
.method .sourcecode p.source-link {
text-indent: 0em;
margin-top: 0.5em;
}
.method .aka {
margin-top: 0.3em;
margin-left: 1em;
font-style: italic;
text-indent: 2em;
}
h1 {
padding: 1em;
border: 1px solid black;
font-size: x-large;
font-weight: bold;
color: #FFF;
background: #007;
}
h2 {
padding: 0.5em 1em 0.5em 1em;
border: 1px solid black;
font-size: large;
font-weight: bold;
color: #FFF;
background: #009;
}
h3, h4, h5, h6 {
padding: 0.2em 1em 0.2em 1em;
border: 1px dashed black;
color: #000;
background: #AAF;
}
.sourcecode > pre {
padding: 0.5em;
border: 1px dotted black;
background: #FFE;
}
CSS
XHTML_PREAMBLE = %{<?xml version="1.0" encoding="%charset%"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
}
HEADER = XHTML_PREAMBLE + <<ENDHEADER
<html>
<head>
<title>%title%</title>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%" />
<link rel="stylesheet" href="%style_url%" type="text/css" media="screen" />
<script language="JavaScript" type="text/javascript">
// <![CDATA[
function toggleSource( id )
{
var elem
var link
if( document.getElementById )
{
elem = document.getElementById( id )
link = document.getElementById( "l_" + id )
}
else if ( document.all )
{
elem = eval( "document.all." + id )
link = eval( "document.all.l_" + id )
}
else
return false;
if( elem.style.display == "block" )
{
elem.style.display = "none"
link.innerHTML = "show source"
}
else
{
elem.style.display = "block"
link.innerHTML = "hide source"
}
}
function openCode( url )
{
window.open( url, "SOURCE_CODE", "width=400,height=400,scrollbars=yes" )
}
// ]]>
</script>
</head>
<body>
ENDHEADER
FILE_PAGE = <<HTML
<table border='0' cellpadding='0' cellspacing='0' width="100%" class='banner'>
<tr><td>
<table width="100%" border='0' cellpadding='0' cellspacing='0'><tr>
<td class="file-title" colspan="2"><span class="file-title-prefix">File</span><br />%short_name%</td>
<td align="right">
<table border='0' cellspacing="0" cellpadding="2">
<tr>
<td>Path:</td>
<td>%full_path%
IF:cvsurl
&nbsp;(<a href="%cvsurl%">CVS</a>)
ENDIF:cvsurl
</td>
</tr>
<tr>
<td>Modified:</td>
<td>%dtm_modified%</td>
</tr>
</table>
</td></tr>
</table>
</td></tr>
</table><br>
HTML
###################################################################
CLASS_PAGE = <<HTML
<table width="100%" border='0' cellpadding='0' cellspacing='0' class='banner'><tr>
<td class="file-title"><span class="file-title-prefix">%classmod%</span><br />%full_name%</td>
<td align="right">
<table cellspacing=0 cellpadding=2>
<tr valign="top">
<td>In:</td>
<td>
START:infiles
HREF:full_path_url:full_path:
IF:cvsurl
&nbsp;(<a href="%cvsurl%">CVS</a>)
ENDIF:cvsurl
END:infiles
</td>
</tr>
IF:parent
<tr>
<td>Parent:</td>
<td>
IF:par_url
<a href="%par_url%">
ENDIF:par_url
%parent%
IF:par_url
</a>
ENDIF:par_url
</td>
</tr>
ENDIF:parent
</table>
</td>
</tr>
</table>
HTML
###################################################################
METHOD_LIST = <<HTML
<div id="content">
IF:diagram
<table cellpadding='0' cellspacing='0' border='0' width="100%"><tr><td align="center">
%diagram%
</td></tr></table>
ENDIF:diagram
IF:description
<div class="description">%description%</div>
ENDIF:description
IF:requires
<div class="sectiontitle">Required Files</div>
<ul>
START:requires
<li>HREF:aref:name:</li>
END:requires
</ul>
ENDIF:requires
IF:toc
<div class="sectiontitle">Contents</div>
<ul>
START:toc
<li><a href="#%href%">%secname%</a></li>
END:toc
</ul>
ENDIF:toc
IF:methods
<div class="sectiontitle">Methods</div>
<ul>
START:methods
<li>HREF:aref:name:</li>
END:methods
</ul>
ENDIF:methods
IF:includes
<div class="sectiontitle">Included Modules</div>
<ul>
START:includes
<li>HREF:aref:name:</li>
END:includes
</ul>
ENDIF:includes
START:sections
IF:sectitle
<div class="sectiontitle"><a nem="%secsequence%">%sectitle%</a></div>
IF:seccomment
<div class="description">
%seccomment%
</div>
ENDIF:seccomment
ENDIF:sectitle
IF:classlist
<div class="sectiontitle">Classes and Modules</div>
%classlist%
ENDIF:classlist
IF:constants
<div class="sectiontitle">Constants</div>
<table border='0' cellpadding='5'>
START:constants
<tr valign='top'>
<td class="attr-name">%name%</td>
<td>=</td>
<td class="attr-value">%value%</td>
</tr>
IF:desc
<tr valign='top'>
<td>&nbsp;</td>
<td colspan="2" class="attr-desc">%desc%</td>
</tr>
ENDIF:desc
END:constants
</table>
ENDIF:constants
IF:attributes
<div class="sectiontitle">Attributes</div>
<table border='0' cellpadding='5'>
START:attributes
<tr valign='top'>
<td class='attr-rw'>
IF:rw
[%rw%]
ENDIF:rw
</td>
<td class='attr-name'>%name%</td>
<td class='attr-desc'>%a_desc%</td>
</tr>
END:attributes
</table>
ENDIF:attributes
IF:method_list
START:method_list
IF:methods
<div class="sectiontitle">%type% %category% methods</div>
START:methods
<div class="method">
<div class="title">
IF:callseq
<a name="%aref%"></a><b>%callseq%</b>
ENDIF:callseq
IFNOT:callseq
<a name="%aref%"></a><b>%name%</b>%params%
ENDIF:callseq
IF:codeurl
[ <a href="javascript:openCode('%codeurl%')">source</a> ]
ENDIF:codeurl
</div>
IF:m_desc
<div class="description">
%m_desc%
</div>
ENDIF:m_desc
IF:aka
<div class="aka">
This method is also aliased as
START:aka
<a href="%aref%">%name%</a>
END:aka
</div>
ENDIF:aka
IF:sourcecode
<div class="sourcecode">
<p class="source-link">[ <a href="javascript:toggleSource('%aref%_source')" id="l_%aref%_source">show source</a> ]</p>
<div id="%aref%_source" class="dyn-source">
<pre>
%sourcecode%
</pre>
</div>
</div>
ENDIF:sourcecode
</div>
END:methods
ENDIF:methods
END:method_list
ENDIF:method_list
END:sections
</div>
HTML
FOOTER = <<ENDFOOTER
</body>
</html>
ENDFOOTER
BODY = HEADER + <<ENDBODY
!INCLUDE! <!-- banner header -->
<div id="bodyContent">
#{METHOD_LIST}
</div>
#{FOOTER}
ENDBODY
########################## Source code ##########################
SRC_PAGE = XHTML_PREAMBLE + <<HTML
<html>
<head><title>%title%</title>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%">
<style>
.ruby-comment { color: green; font-style: italic }
.ruby-constant { color: #4433aa; font-weight: bold; }
.ruby-identifier { color: #222222; }
.ruby-ivar { color: #2233dd; }
.ruby-keyword { color: #3333FF; font-weight: bold }
.ruby-node { color: #777777; }
.ruby-operator { color: #111111; }
.ruby-regexp { color: #662222; }
.ruby-value { color: #662222; font-style: italic }
.kw { color: #3333FF; font-weight: bold }
.cmt { color: green; font-style: italic }
.str { color: #662222; font-style: italic }
.re { color: #662222; }
</style>
</head>
<body bgcolor="white">
<pre>%code%</pre>
</body>
</html>
HTML
########################## Index ################################
FR_INDEX_BODY = <<HTML
!INCLUDE!
HTML
FILE_INDEX = XHTML_PREAMBLE + <<HTML
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%">
<style>
<!--
body {
background-color: #EEE;
font-family: #{FONTS};
color: #000;
margin: 0px;
}
.banner {
background: #005;
color: #FFF;
padding: 0.2em;
font-size: small;
font-weight: bold;
text-align: center;
}
.entries {
margin: 0.25em 1em 0 1em;
font-size: x-small;
}
a {
color: #00F;
text-decoration: none;
white-space: nowrap;
}
a:hover {
color: #77F;
text-decoration: underline;
}
-->
</style>
<base target="docwin">
</head>
<body>
<div class="banner">%list_title%</div>
<div class="entries">
START:entries
<a href="%href%">%name%</a><br>
END:entries
</div>
</body></html>
HTML
CLASS_INDEX = FILE_INDEX
METHOD_INDEX = FILE_INDEX
INDEX = XHTML_PREAMBLE + <<HTML
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>%title%</title>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%">
</head>
<frameset cols="20%,*">
<frameset rows="15%,35%,50%">
<frame src="fr_file_index.html" title="Files" name="Files" />
<frame src="fr_class_index.html" name="Classes" />
<frame src="fr_method_index.html" name="Methods" />
</frameset>
IF:inline_source
<frame src="%initial_page%" name="docwin">
ENDIF:inline_source
IFNOT:inline_source
<frameset rows="80%,20%">
<frame src="%initial_page%" name="docwin">
<frame src="blank.html" name="source">
</frameset>
ENDIF:inline_source
<noframes>
<body bgcolor="white">
Click <a href="html/index.html">here</a> for a non-frames
version of this page.
</body>
</noframes>
</frameset>
</html>
HTML
end
end

@ -0,0 +1,31 @@
= Builder 1.2.4 Released.
Added a "CDATA" method to the XML Markup builder (from Josh Knowles).
== What is Builder?
Builder::XmlMarkup allows easy programmatic creation of XML markup.
For example:
builder = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
builder.person { |b| b.name("Jim"); b.phone("555-1234") }
puts builder.target!
will generate:
<person>
<name>Jim</name>
<phone>555-1234</phone>
</person>
== Availability
The easiest way to get and install builder is via RubyGems ...
gem install builder (you may need root/admin privileges)
== Thanks
* Josh Knowles for the cdata! patch.
-- Jim Weirich

@ -0,0 +1,46 @@
= Builder 2.0.0 Released.
== Changes in 2.0.0
* UTF-8 characters in data are now correctly translated to their XML
equivalents. (Thanks to Sam Ruby)
* Attribute values are now escaped by default. See the README
file for details.
<b>NOTE:</b> The escaping attribute values by default is different
than in previous releases of Builder. This makes version 2.0.0
somewhat incompatible with the 1.x series of Builder. If you use "&",
"<", or ">" in attributes values, you may have to change your
code. (Essentially you remove the manual escaping. The new way is
easier, believe me).
== What is Builder?
Builder::XmlMarkup is a library that allows easy programmatic creation
of XML markup. For example:
builder = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
builder.person { |b| b.name("Jim"); b.phone("555-1234") }
will generate:
<person>
<name>Jim</name>
<phone>555-1234</phone>
</person>
== Availability
The easiest way to get and install builder is via RubyGems ...
gem install builder (you may need root/admin privileges)
== Thanks
* Sam Ruby for the XChar module and the related UTF-8 translation
tools.
* Also to Sam Ruby for gently persuading me to start quoting attribute
values.
-- Jim Weirich

@ -0,0 +1,58 @@
= Builder 2.1.1 Released.
Release 2.1.1 of Builder is mainly a bug fix release.
== Changes in 2.1.1
* Added <tt>reveal</tt> capability to BlankSlate.
* Fixed a bug in BlankSlate where including a module into Object could
cause methods to leak into BlankSlate.
* Fixed typo in XmlMarkup class docs (from Martin Fowler).
* Fixed test on private methods to differentiate between targetted and
untargetted private methods.
* Removed legacy capture of @self in XmlBase (@self was used back when
we used instance eval).
* Added additional tests for global functions (both direct and
included).
* Several misc internal cleanups, including rearranging the source
code tree.
<b>NOTE:</b> The escaping attribute values by default is different
than in previous releases of Builder. This makes version 2.0.x
somewhat incompatible with the 1.x series of Builder. If you use "&",
"<", or ">" in attributes values, you may have to change your
code. (Essentially you remove the manual escaping. The new way is
easier, believe me).
== What is Builder?
Builder::XmlMarkup is a library that allows easy programmatic creation
of XML markup. For example:
builder = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
builder.person { |b| b.name("Jim"); b.phone("555-1234") }
will generate:
<person>
<name>Jim</name>
<phone>555-1234</phone>
</person>
== Availability
The easiest way to get and install builder is via RubyGems ...
gem install builder (you may need root/admin privileges)
== Thanks
* Martin Fowler for spotting some typos in the documentation.
-- Jim Weirich

@ -0,0 +1,137 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
class String
if instance_methods.first.is_a?(Symbol)
def _blankslate_as_name
to_sym
end
else
def _blankslate_as_name
self
end
end
end
class Symbol
if instance_methods.first.is_a?(Symbol)
def _blankslate_as_name
self
end
else
def _blankslate_as_name
to_s
end
end
end
######################################################################
# BlankSlate provides an abstract base class with no predefined
# methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
# BlankSlate is useful as a base class when writing classes that
# depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
#
class BlankSlate
class << self
# Hide the method named +name+ in the BlankSlate class. Don't
# hide +instance_eval+ or any method beginning with "__".
def hide(name)
warn_level = $VERBOSE
$VERBOSE = nil
if instance_methods.include?(name._blankslate_as_name) &&
name !~ /^(__|instance_eval$)/
@hidden_methods ||= {}
@hidden_methods[name.to_sym] = instance_method(name)
undef_method name
end
ensure
$VERBOSE = warn_level
end
def find_hidden_method(name)
@hidden_methods ||= {}
@hidden_methods[name] || superclass.find_hidden_method(name)
end
# Redefine a previously hidden method so that it may be called on a blank
# slate object.
def reveal(name)
hidden_method = find_hidden_method(name)
fail "Don't know how to reveal method '#{name}'" unless hidden_method
define_method(name, hidden_method)
end
end
instance_methods.each { |m| hide(m) }
end
######################################################################
# Since Ruby is very dynamic, methods added to the ancestors of
# BlankSlate <em>after BlankSlate is defined</em> will show up in the
# list of available BlankSlate methods. We handle this by defining a
# hook in the Object and Kernel classes that will hide any method
# defined after BlankSlate has been loaded.
#
module Kernel
class << self
alias_method :blank_slate_method_added, :method_added
# Detect method additions to Kernel and remove them in the
# BlankSlate class.
def method_added(name)
result = blank_slate_method_added(name)
return result if self != Kernel
BlankSlate.hide(name)
result
end
end
end
######################################################################
# Same as above, except in Object.
#
class Object
class << self
alias_method :blank_slate_method_added, :method_added
# Detect method additions to Object and remove them in the
# BlankSlate class.
def method_added(name)
result = blank_slate_method_added(name)
return result if self != Object
BlankSlate.hide(name)
result
end
def find_hidden_method(name)
nil
end
end
end
######################################################################
# Also, modules included into Object need to be scanned and have their
# instance methods removed from blank slate. In theory, modules
# included into Kernel would have to be removed as well, but a
# "feature" of Ruby prevents late includes into modules from being
# exposed in the first place.
#
class Module
alias blankslate_original_append_features append_features
def append_features(mod)
result = blankslate_original_append_features(mod)
return result if mod != Object
instance_methods.each do |name|
BlankSlate.hide(name)
end
result
end
end

@ -0,0 +1,13 @@
#!/usr/bin/env ruby
#--
# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'builder/xmlmarkup'
require 'builder/xmlevents'

@ -0,0 +1,23 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
######################################################################
# BlankSlate has been promoted to a top level name and is now
# available as a standalone gem. We make the name available in the
# Builder namespace for compatibility.
#
module Builder
if Object::const_defined?(:BasicObject)
BlankSlate = ::BasicObject
else
require 'blankslate'
BlankSlate = ::BlankSlate
end
end

@ -0,0 +1,8 @@
module Builder
VERSION_NUMBERS = [
VERSION_MAJOR = 3,
VERSION_MINOR = 2,
VERSION_BUILD = 2,
]
VERSION = VERSION_NUMBERS.join(".")
end

@ -0,0 +1,197 @@
#!/usr/bin/env ruby
# The XChar library is provided courtesy of Sam Ruby (See
# http://intertwingly.net/stories/2005/09/28/xchar.rb)
# --------------------------------------------------------------------
# If the Builder::XChar module is not currently defined, fail on any
# name clashes in standard library classes.
module Builder
def self.check_for_name_collision(klass, method_name, defined_constant=nil)
if klass.method_defined?(method_name.to_s)
fail RuntimeError,
"Name Collision: Method '#{method_name}' is already defined in #{klass}"
end
end
end
if ! defined?(Builder::XChar) and ! String.method_defined?(:encode)
Builder.check_for_name_collision(String, "to_xs")
Builder.check_for_name_collision(Fixnum, "xchr")
end
######################################################################
module Builder
####################################################################
# XML Character converter, from Sam Ruby:
# (see http://intertwingly.net/stories/2005/09/28/xchar.rb).
#
module XChar # :nodoc:
# See
# http://intertwingly.net/stories/2004/04/14/i18n.html#CleaningWindows
# for details.
CP1252 = { # :nodoc:
128 => 8364, # euro sign
130 => 8218, # single low-9 quotation mark
131 => 402, # latin small letter f with hook
132 => 8222, # double low-9 quotation mark
133 => 8230, # horizontal ellipsis
134 => 8224, # dagger
135 => 8225, # double dagger
136 => 710, # modifier letter circumflex accent
137 => 8240, # per mille sign
138 => 352, # latin capital letter s with caron
139 => 8249, # single left-pointing angle quotation mark
140 => 338, # latin capital ligature oe
142 => 381, # latin capital letter z with caron
145 => 8216, # left single quotation mark
146 => 8217, # right single quotation mark
147 => 8220, # left double quotation mark
148 => 8221, # right double quotation mark
149 => 8226, # bullet
150 => 8211, # en dash
151 => 8212, # em dash
152 => 732, # small tilde
153 => 8482, # trade mark sign
154 => 353, # latin small letter s with caron
155 => 8250, # single right-pointing angle quotation mark
156 => 339, # latin small ligature oe
158 => 382, # latin small letter z with caron
159 => 376, # latin capital letter y with diaeresis
}
# See http://www.w3.org/TR/REC-xml/#dt-chardata for details.
PREDEFINED = {
38 => '&amp;', # ampersand
60 => '&lt;', # left angle bracket
62 => '&gt;', # right angle bracket
}
# See http://www.w3.org/TR/REC-xml/#charsets for details.
VALID = [
0x9, 0xA, 0xD,
(0x20..0xD7FF),
(0xE000..0xFFFD),
(0x10000..0x10FFFF)
]
# http://www.fileformat.info/info/unicode/char/fffd/index.htm
REPLACEMENT_CHAR =
if String.method_defined?(:encode)
"\uFFFD"
elsif $KCODE == 'UTF8'
"\xEF\xBF\xBD"
else
'*'
end
end
end
if String.method_defined?(:encode)
module Builder
module XChar # :nodoc:
CP1252_DIFFERENCES, UNICODE_EQUIVALENT = Builder::XChar::CP1252.each.
inject([[],[]]) {|(domain,range),(key,value)|
[domain << key,range << value]
}.map {|seq| seq.pack('U*').force_encoding('utf-8')}
XML_PREDEFINED = Regexp.new('[' +
Builder::XChar::PREDEFINED.keys.pack('U*').force_encoding('utf-8') +
']')
INVALID_XML_CHAR = Regexp.new('[^'+
Builder::XChar::VALID.map { |item|
case item
when Fixnum
[item].pack('U').force_encoding('utf-8')
when Range
[item.first, '-'.ord, item.last].pack('UUU').force_encoding('utf-8')
end
}.join +
']')
ENCODING_BINARY = Encoding.find('BINARY')
ENCODING_UTF8 = Encoding.find('UTF-8')
ENCODING_ISO1 = Encoding.find('ISO-8859-1')
# convert a string to valid UTF-8, compensating for a number of
# common errors.
def XChar.unicode(string)
if string.encoding == ENCODING_BINARY
if string.ascii_only?
string
else
string = string.clone.force_encoding(ENCODING_UTF8)
if string.valid_encoding?
string
else
string.encode(ENCODING_UTF8, ENCODING_ISO1)
end
end
elsif string.encoding == ENCODING_UTF8
if string.valid_encoding?
string
else
string.encode(ENCODING_UTF8, ENCODING_ISO1)
end
else
string.encode(ENCODING_UTF8)
end
end
# encode a string per XML rules
def XChar.encode(string)
unicode(string).
tr(CP1252_DIFFERENCES, UNICODE_EQUIVALENT).
gsub(INVALID_XML_CHAR, REPLACEMENT_CHAR).
gsub(XML_PREDEFINED) {|c| PREDEFINED[c.ord]}
end
end
end
else
######################################################################
# Enhance the Fixnum class with a XML escaped character conversion.
#
class Fixnum
XChar = Builder::XChar if ! defined?(XChar)
# XML escaped version of chr. When <tt>escape</tt> is set to false
# the CP1252 fix is still applied but utf-8 characters are not
# converted to character entities.
def xchr(escape=true)
n = XChar::CP1252[self] || self
case n when *XChar::VALID
XChar::PREDEFINED[n] or
(n<128 ? n.chr : (escape ? "&##{n};" : [n].pack('U*')))
else
Builder::XChar::REPLACEMENT_CHAR
end
end
end
######################################################################
# Enhance the String class with a XML escaped character version of
# to_s.
#
class String
# XML escaped version of to_s. When <tt>escape</tt> is set to false
# the CP1252 fix is still applied but utf-8 characters are not
# converted to character entities.
def to_xs(escape=true)
unpack('U*').map {|n| n.xchr(escape)}.join # ASCII, UTF-8
rescue
unpack('C*').map {|n| n.xchr}.join # ISO-8859-1, WIN-1252
end
end
end

@ -0,0 +1,199 @@
#!/usr/bin/env ruby
require 'builder/blankslate'
module Builder
# Generic error for builder
class IllegalBlockError < RuntimeError; end
# XmlBase is a base class for building XML builders. See
# Builder::XmlMarkup and Builder::XmlEvents for examples.
class XmlBase < BlankSlate
class << self
attr_accessor :cache_method_calls
end
# Create an XML markup builder.
#
# out :: Object receiving the markup. +out+ must respond to
# <tt><<</tt>.
# indent :: Number of spaces used for indentation (0 implies no
# indentation and no line breaks).
# initial :: Level of initial indentation.
# encoding :: When <tt>encoding</tt> and $KCODE are set to 'utf-8'
# characters aren't converted to character entities in
# the output stream.
def initialize(indent=0, initial=0, encoding='utf-8')
@indent = indent
@level = initial
@encoding = encoding.downcase
end
def explicit_nil_handling?
@explicit_nil_handling
end
# Create a tag named +sym+. Other than the first argument which
# is the tag name, the arguments are the same as the tags
# implemented via <tt>method_missing</tt>.
def tag!(sym, *args, &block)
text = nil
attrs = nil
sym = "#{sym}:#{args.shift}" if args.first.kind_of?(::Symbol)
sym = sym.to_sym unless sym.class == ::Symbol
args.each do |arg|
case arg
when ::Hash
attrs ||= {}
attrs.merge!(arg)
when nil
attrs ||= {}
attrs.merge!({:nil => true}) if explicit_nil_handling?
else
text ||= ''
text << arg.to_s
end
end
if block
unless text.nil?
::Kernel::raise ::ArgumentError,
"XmlMarkup cannot mix a text argument with a block"
end
_indent
_start_tag(sym, attrs)
_newline
begin
_nested_structures(block)
ensure
_indent
_end_tag(sym)
_newline
end
elsif text.nil?
_indent
_start_tag(sym, attrs, true)
_newline
else
_indent
_start_tag(sym, attrs)
text! text
_end_tag(sym)
_newline
end
@target
end
# Create XML markup based on the name of the method. This method
# is never invoked directly, but is called for each markup method
# in the markup block that isn't cached.
def method_missing(sym, *args, &block)
cache_method_call(sym) if ::Builder::XmlBase.cache_method_calls
tag!(sym, *args, &block)
end
# Append text to the output target. Escape any markup. May be
# used within the markup brackets as:
#
# builder.p { |b| b.br; b.text! "HI" } #=> <p><br/>HI</p>
def text!(text)
_text(_escape(text))
end
# Append text to the output target without escaping any markup.
# May be used within the markup brackets as:
#
# builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p>
#
# This is useful when using non-builder enabled software that
# generates strings. Just insert the string directly into the
# builder without changing the inserted markup.
#
# It is also useful for stacking builder objects. Builders only
# use <tt><<</tt> to append to the target, so by supporting this
# method/operation builders can use other builders as their
# targets.
def <<(text)
_text(text)
end
# For some reason, nil? is sent to the XmlMarkup object. If nil?
# is not defined and method_missing is invoked, some strange kind
# of recursion happens. Since nil? won't ever be an XML tag, it
# is pretty safe to define it here. (Note: this is an example of
# cargo cult programming,
# cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
def nil?
false
end
private
require 'builder/xchar'
if ::String.method_defined?(:encode)
def _escape(text)
result = XChar.encode(text)
begin
encoding = ::Encoding::find(@encoding)
raise Exception if encoding.dummy?
result.encode(encoding)
rescue
# if the encoding can't be supported, use numeric character references
result.
gsub(/[^\u0000-\u007F]/) {|c| "&##{c.ord};"}.
force_encoding('ascii')
end
end
else
def _escape(text)
if (text.method(:to_xs).arity == 0)
text.to_xs
else
text.to_xs((@encoding != 'utf-8' or $KCODE != 'UTF8'))
end
end
end
def _escape_attribute(text)
_escape(text).gsub("\n", "&#10;").gsub("\r", "&#13;").
gsub(%r{"}, '&quot;') # " WART
end
def _newline
return if @indent == 0
text! "\n"
end
def _indent
return if @indent == 0 || @level == 0
text!(" " * (@level * @indent))
end
def _nested_structures(block)
@level += 1
block.call(self)
ensure
@level -= 1
end
# If XmlBase.cache_method_calls = true, we dynamicly create the method
# missed as an instance method on the XMLBase object. Because XML
# documents are usually very repetative in nature, the next node will
# be handled by the new method instead of method_missing. As
# method_missing is very slow, this speeds up document generation
# significantly.
def cache_method_call(sym)
class << self; self; end.class_eval do
unless method_defined?(sym)
define_method(sym) do |*args, &block|
tag!(sym, *args, &block)
end
end
end
end
end
XmlBase.cache_method_calls = true
end

@ -0,0 +1,63 @@
#!/usr/bin/env ruby
#--
# Copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'builder/xmlmarkup'
module Builder
# Create a series of SAX-like XML events (e.g. start_tag, end_tag)
# from the markup code. XmlEvent objects are used in a way similar
# to XmlMarkup objects, except that a series of events are generated
# and passed to a handler rather than generating character-based
# markup.
#
# Usage:
# xe = Builder::XmlEvents.new(hander)
# xe.title("HI") # Sends start_tag/end_tag/text messages to the handler.
#
# Indentation may also be selected by providing value for the
# indentation size and initial indentation level.
#
# xe = Builder::XmlEvents.new(handler, indent_size, initial_indent_level)
#
# == XML Event Handler
#
# The handler object must expect the following events.
#
# [<tt>start_tag(tag, attrs)</tt>]
# Announces that a new tag has been found. +tag+ is the name of
# the tag and +attrs+ is a hash of attributes for the tag.
#
# [<tt>end_tag(tag)</tt>]
# Announces that an end tag for +tag+ has been found.
#
# [<tt>text(text)</tt>]
# Announces that a string of characters (+text+) has been found.
# A series of characters may be broken up into more than one
# +text+ call, so the client cannot assume that a single
# callback contains all the text data.
#
class XmlEvents < XmlMarkup
def text!(text)
@target.text(text)
end
def _start_tag(sym, attrs, end_too=false)
@target.start_tag(sym, attrs)
_end_tag(sym) if end_too
end
def _end_tag(sym)
@target.end_tag(sym)
end
end
end

@ -0,0 +1,339 @@
#!/usr/bin/env ruby
#--
# Copyright 2004, 2005 by Jim Weirich (jim@weirichhouse.org).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
# Provide a flexible and easy to use Builder for creating XML markup.
# See XmlBuilder for usage details.
require 'builder/xmlbase'
module Builder
# Create XML markup easily. All (well, almost all) methods sent to
# an XmlMarkup object will be translated to the equivalent XML
# markup. Any method with a block will be treated as an XML markup
# tag with nested markup in the block.
#
# Examples will demonstrate this easier than words. In the
# following, +xm+ is an +XmlMarkup+ object.
#
# xm.em("emphasized") # => <em>emphasized</em>
# xm.em { xm.b("emp & bold") } # => <em><b>emph &amp; bold</b></em>
# xm.a("A Link", "href"=>"http://onestepback.org")
# # => <a href="http://onestepback.org">A Link</a>
# xm.div { xm.br } # => <div><br/></div>
# xm.target("name"=>"compile", "option"=>"fast")
# # => <target option="fast" name="compile"\>
# # NOTE: order of attributes is not specified.
#
# xm.instruct! # <?xml version="1.0" encoding="UTF-8"?>
# xm.html { # <html>
# xm.head { # <head>
# xm.title("History") # <title>History</title>
# } # </head>
# xm.body { # <body>
# xm.comment! "HI" # <!-- HI -->
# xm.h1("Header") # <h1>Header</h1>
# xm.p("paragraph") # <p>paragraph</p>
# } # </body>
# } # </html>
#
# == Notes:
#
# * The order that attributes are inserted in markup tags is
# undefined.
#
# * Sometimes you wish to insert text without enclosing tags. Use
# the <tt>text!</tt> method to accomplish this.
#
# Example:
#
# xm.div { # <div>
# xm.text! "line"; xm.br # line<br/>
# xm.text! "another line"; xmbr # another line<br/>
# } # </div>
#
# * The special XML characters <, >, and & are converted to &lt;,
# &gt; and &amp; automatically. Use the <tt><<</tt> operation to
# insert text without modification.
#
# * Sometimes tags use special characters not allowed in ruby
# identifiers. Use the <tt>tag!</tt> method to handle these
# cases.
#
# Example:
#
# xml.tag!("SOAP:Envelope") { ... }
#
# will produce ...
#
# <SOAP:Envelope> ... </SOAP:Envelope>"
#
# <tt>tag!</tt> will also take text and attribute arguments (after
# the tag name) like normal markup methods. (But see the next
# bullet item for a better way to handle XML namespaces).
#
# * Direct support for XML namespaces is now available. If the
# first argument to a tag call is a symbol, it will be joined to
# the tag to produce a namespace:tag combination. It is easier to
# show this than describe it.
#
# xml.SOAP :Envelope do ... end
#
# Just put a space before the colon in a namespace to produce the
# right form for builder (e.g. "<tt>SOAP:Envelope</tt>" =>
# "<tt>xml.SOAP :Envelope</tt>")
#
# * XmlMarkup builds the markup in any object (called a _target_)
# that accepts the <tt><<</tt> method. If no target is given,
# then XmlMarkup defaults to a string target.
#
# Examples:
#
# xm = Builder::XmlMarkup.new
# result = xm.title("yada")
# # result is a string containing the markup.
#
# buffer = ""
# xm = Builder::XmlMarkup.new(buffer)
# # The markup is appended to buffer (using <<)
#
# xm = Builder::XmlMarkup.new(STDOUT)
# # The markup is written to STDOUT (using <<)
#
# xm = Builder::XmlMarkup.new
# x2 = Builder::XmlMarkup.new(:target=>xm)
# # Markup written to +x2+ will be send to +xm+.
#
# * Indentation is enabled by providing the number of spaces to
# indent for each level as a second argument to XmlBuilder.new.
# Initial indentation may be specified using a third parameter.
#
# Example:
#
# xm = Builder.new(:indent=>2)
# # xm will produce nicely formatted and indented XML.
#
# xm = Builder.new(:indent=>2, :margin=>4)
# # xm will produce nicely formatted and indented XML with 2
# # spaces per indent and an over all indentation level of 4.
#
# builder = Builder::XmlMarkup.new(:target=>$stdout, :indent=>2)
# builder.name { |b| b.first("Jim"); b.last("Weirich) }
# # prints:
# # <name>
# # <first>Jim</first>
# # <last>Weirich</last>
# # </name>
#
# * The instance_eval implementation which forces self to refer to
# the message receiver as self is now obsolete. We now use normal
# block calls to execute the markup block. This means that all
# markup methods must now be explicitly send to the xml builder.
# For instance, instead of
#
# xml.div { strong("text") }
#
# you need to write:
#
# xml.div { xml.strong("text") }
#
# Although more verbose, the subtle change in semantics within the
# block was found to be prone to error. To make this change a
# little less cumbersome, the markup block now gets the markup
# object sent as an argument, allowing you to use a shorter alias
# within the block.
#
# For example:
#
# xml_builder = Builder::XmlMarkup.new
# xml_builder.div { |xml|
# xml.stong("text")
# }
#
class XmlMarkup < XmlBase
# Create an XML markup builder. Parameters are specified by an
# option hash.
#
# :target => <em>target_object</em>::
# Object receiving the markup. +target_object+ must respond to
# the <tt><<(<em>a_string</em>)</tt> operator and return
# itself. The default target is a plain string target.
#
# :indent => <em>indentation</em>::
# Number of spaces used for indentation. The default is no
# indentation and no line breaks.
#
# :margin => <em>initial_indentation_level</em>::
# Amount of initial indentation (specified in levels, not
# spaces).
#
# :quote => <em>:single</em>::
# Use single quotes for attributes rather than double quotes.
#
# :escape_attrs => <em>OBSOLETE</em>::
# The :escape_attrs option is no longer supported by builder
# (and will be quietly ignored). String attribute values are
# now automatically escaped. If you need unescaped attribute
# values (perhaps you are using entities in the attribute
# values), then give the value as a Symbol. This allows much
# finer control over escaping attribute values.
#
def initialize(options={})
indent = options[:indent] || 0
margin = options[:margin] || 0
@quote = (options[:quote] == :single) ? "'" : '"'
@explicit_nil_handling = options[:explicit_nil_handling]
super(indent, margin)
@target = options[:target] || ""
end
# Return the target of the builder.
def target!
@target
end
def comment!(comment_text)
_ensure_no_block ::Kernel::block_given?
_special("<!-- ", " -->", comment_text, nil)
end
# Insert an XML declaration into the XML markup.
#
# For example:
#
# xml.declare! :ELEMENT, :blah, "yada"
# # => <!ELEMENT blah "yada">
def declare!(inst, *args, &block)
_indent
@target << "<!#{inst}"
args.each do |arg|
case arg
when ::String
@target << %{ "#{arg}"} # " WART
when ::Symbol
@target << " #{arg}"
end
end
if ::Kernel::block_given?
@target << " ["
_newline
_nested_structures(block)
@target << "]"
end
@target << ">"
_newline
end
# Insert a processing instruction into the XML markup. E.g.
#
# For example:
#
# xml.instruct!
# #=> <?xml version="1.0" encoding="UTF-8"?>
# xml.instruct! :aaa, :bbb=>"ccc"
# #=> <?aaa bbb="ccc"?>
#
# Note: If the encoding is setup to "UTF-8" and the value of
# $KCODE is "UTF8", then builder will emit UTF-8 encoded strings
# rather than the entity encoding normally used.
def instruct!(directive_tag=:xml, attrs={})
_ensure_no_block ::Kernel::block_given?
if directive_tag == :xml
a = { :version=>"1.0", :encoding=>"UTF-8" }
attrs = a.merge attrs
@encoding = attrs[:encoding].downcase
end
_special(
"<?#{directive_tag}",
"?>",
nil,
attrs,
[:version, :encoding, :standalone])
end
# Insert a CDATA section into the XML markup.
#
# For example:
#
# xml.cdata!("text to be included in cdata")
# #=> <![CDATA[text to be included in cdata]]>
#
def cdata!(text)
_ensure_no_block ::Kernel::block_given?
_special("<![CDATA[", "]]>", text.gsub(']]>', ']]]]><![CDATA[>'), nil)
end
private
# NOTE: All private methods of a builder object are prefixed when
# a "_" character to avoid possible conflict with XML tag names.
# Insert text directly in to the builder's target.
def _text(text)
@target << text
end
# Insert special instruction.
def _special(open, close, data=nil, attrs=nil, order=[])
_indent
@target << open
@target << data if data
_insert_attributes(attrs, order) if attrs
@target << close
_newline
end
# Start an XML tag. If <tt>end_too</tt> is true, then the start
# tag is also the end tag (e.g. <br/>
def _start_tag(sym, attrs, end_too=false)
@target << "<#{sym}"
_insert_attributes(attrs)
@target << "/" if end_too
@target << ">"
end
# Insert an ending tag.
def _end_tag(sym)
@target << "</#{sym}>"
end
# Insert the attributes (given in the hash).
def _insert_attributes(attrs, order=[])
return if attrs.nil?
order.each do |k|
v = attrs[k]
@target << %{ #{k}=#{@quote}#{_attr_value(v)}#{@quote}} if v
end
attrs.each do |k, v|
@target << %{ #{k}=#{@quote}#{_attr_value(v)}#{@quote}} unless order.member?(k) # " WART
end
end
def _attr_value(value)
case value
when ::Symbol
value.to_s
else
_escape_attribute(value.to_s)
end
end
def _ensure_no_block(got_block)
if got_block
::Kernel::raise IllegalBlockError.new(
"Blocks are not allowed on XML instructions"
)
end
end
end
end

@ -0,0 +1,17 @@
# Optional publish task for Rake
require 'rake/contrib/sshpublisher'
require 'rake/contrib/rubyforgepublisher'
publisher = Rake::CompositePublisher.new
publisher.add Rake::RubyForgePublisher.new('builder', 'jimweirich')
publisher.add Rake::SshFilePublisher.new(
'linode',
'htdocs/software/builder',
'.',
'builder.blurb')
desc "Publish the Documentation to RubyForge."
task :publish => [:rdoc] do
publisher.upload
end

@ -0,0 +1,62 @@
#!/usr/bin/env ruby
module Tags
extend Rake::DSL if defined?(Rake::DSL)
PROG = ENV['TAGS'] || 'ctags'
RAKEFILES = FileList['Rakefile', '**/*.rake']
FILES = FileList['**/*.rb', '**/*.js'] + RAKEFILES
FILES.exclude('pkg', 'dist')
PROJECT_DIR = ['.']
RVM_GEMDIR = File.join(`rvm gemdir`.strip, "gems")
SYSTEM_DIRS = File.exists?(RVM_GEMDIR) ? RVM_GEMDIR : []
module_function
# Convert key_word to --key-word.
def keyword(key)
k = key.to_s.gsub(/_/, '-')
(k.length == 1) ? "-#{k}" : "--#{k}"
end
# Run ctags command
def run(*args)
opts = {
:e => true,
:totals => true,
:recurse => true,
}
opts = opts.merge(args.pop) if args.last.is_a?(Hash)
command_args = opts.map { |k, v|
(v == true) ? keyword(k) : "#{keyword(k)}=#{v}"
}.join(" ")
sh %{#{Tags::PROG} #{command_args} #{args.join(' ')}}
end
end
namespace "tags" do
desc "Generate an Emacs TAGS file"
task :emacs, [:all] => Tags::FILES do |t, args|
puts "Making Emacs TAGS file"
verbose(true) do
Tags.run(Tags::PROJECT_DIR)
Tags.run(Tags::RAKEFILES,
:language_force => "ruby",
:append => true)
if args.all
Tags::SYSTEM_DIRS.each do |dir|
Tags.run(dir,
:language_force => "ruby",
:append => true)
end
end
end
end
end
desc "Generate the TAGS file"
task :tags, [:all] => ["tags:emacs"]

@ -0,0 +1,41 @@
#!/usr/bin/env ruby
# encoding: iso-8859-1
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'builder/xmlmarkup'
require 'benchmark'
text = "This is a test of the new xml markup. I<>t<EFBFBD>rn<72>ti<74>n<EFBFBD>liz<69>ti<74>n\n" * 10000
include Benchmark # we need the CAPTION and FMTSTR constants
include Builder
n = 50
Benchmark.benchmark do |bm|
tf = bm.report("base") {
n.times do
x = XmlMarkup.new
x.text(text)
x.target!
end
}
def XmlMarkup._escape(text)
text.to_xs
end
tf = bm.report("to_xs") {
n.times do
x = XmlMarkup.new
x.text(text)
x.target!
end
}
end

@ -0,0 +1,39 @@
#!/usr/bin/env ruby
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
# We are defining method_added in Kernel and Object so that when
# BlankSlate overrides them later, we can verify that it correctly
# calls the older hooks.
module Kernel
class << self
attr_reader :k_added_names
alias_method :preload_method_added, :method_added
def method_added(name)
preload_method_added(name)
@k_added_names ||= []
@k_added_names << name
end
end
end
class Object
class << self
attr_reader :o_added_names
alias_method :preload_method_added, :method_added
def method_added(name)
preload_method_added(name)
@o_added_names ||= []
@o_added_names << name
end
end
end

@ -0,0 +1,217 @@
#!/usr/bin/env ruby
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'test/unit'
require 'test/preload'
require 'blankslate'
require 'stringio'
# Methods to be introduced into the Object class late.
module LateObject
def late_object
33
end
def LateObject.included(mod)
# Modules defining an included method should not prevent blank
# slate erasure!
end
end
# Methods to be introduced into the Kernel module late.
module LateKernel
def late_kernel
44
end
def LateKernel.included(mod)
# Modules defining an included method should not prevent blank
# slate erasure!
end
end
# Introduce some late methods (both module and direct) into the Kernel
# module.
module Kernel
include LateKernel
def late_addition
1234
end
def double_late_addition
11
end
def double_late_addition
22
end
end
# Introduce some late methods (both module and direct) into the Object
# class.
class Object
include LateObject
def another_late_addition
4321
end
end
# Introduce some late methods by inclusion.
module GlobalModule
def global_inclusion
42
end
end
include GlobalModule
def direct_global
43
end
######################################################################
# Test case for blank slate.
#
class TestBlankSlate < Test::Unit::TestCase
def setup
@bs = BlankSlate.new
end
def test_undefined_methods_remain_undefined
assert_raise(NoMethodError) { @bs.no_such_method }
assert_raise(NoMethodError) { @bs.nil? }
end
# NOTE: NameError is acceptable because the lack of a '.' means that
# Ruby can't tell if it is a method or a local variable.
def test_undefined_methods_remain_undefined_during_instance_eval
assert_raise(NoMethodError, NameError) do
@bs.instance_eval do nil? end
end
assert_raise(NoMethodError, NameError) do
@bs.instance_eval do no_such_method end
end
end
def test_private_methods_are_undefined
assert_raise(NoMethodError) do
@bs.puts "HI"
end
end
def test_targetted_private_methods_are_undefined_during_instance_eval
assert_raise(NoMethodError, NameError) do
@bs.instance_eval do self.puts "HI" end
end
end
def test_untargetted_private_methods_are_defined_during_instance_eval
oldstdout = $stdout
$stdout = StringIO.new
@bs.instance_eval do
puts "HI"
end
ensure
$stdout = oldstdout
end
def test_methods_added_late_to_kernel_remain_undefined
assert_equal 1234, nil.late_addition
assert_raise(NoMethodError) { @bs.late_addition }
end
def test_methods_added_late_to_object_remain_undefined
assert_equal 4321, nil.another_late_addition
assert_raise(NoMethodError) { @bs.another_late_addition }
end
def test_methods_added_late_to_global_remain_undefined
assert_equal 42, global_inclusion
assert_raise(NoMethodError) { @bs.global_inclusion }
end
def test_preload_method_added
assert Kernel.k_added_names.include?(:late_addition)
assert Object.o_added_names.include?(:another_late_addition)
end
def test_method_defined_late_multiple_times_remain_undefined
assert_equal 22, nil.double_late_addition
assert_raise(NoMethodError) { @bs.double_late_addition }
end
def test_late_included_module_in_object_is_ok
assert_equal 33, 1.late_object
assert_raise(NoMethodError) { @bs.late_object }
end
def test_late_included_module_in_kernel_is_ok
assert_raise(NoMethodError) { @bs.late_kernel }
end
def test_revealing_previously_hidden_methods_are_callable
with_to_s = Class.new(BlankSlate) do
reveal :to_s
end
assert_match(/^#<.*>$/, with_to_s.new.to_s)
end
def test_revealing_previously_hidden_methods_are_callable_with_block
Object.class_eval <<-EOS
def given_block(&block)
block
end
EOS
with_given_block = Class.new(BlankSlate) do
reveal :given_block
end
assert_not_nil with_given_block.new.given_block {}
end
def test_revealing_a_hidden_method_twice_is_ok
with_to_s = Class.new(BlankSlate) do
reveal :to_s
reveal :to_s
end
assert_match(/^#<.*>$/, with_to_s.new.to_s)
end
def test_revealing_unknown_hidden_method_is_an_error
assert_raises(RuntimeError) do
Class.new(BlankSlate) do
reveal :xyz
end
end
end
def test_global_includes_still_work
assert_nothing_raised do
assert_equal 42, global_inclusion
assert_equal 42, Object.new.global_inclusion
assert_equal 42, "magic number".global_inclusion
assert_equal 43, direct_global
end
end
def test_reveal_should_not_bind_to_an_instance
with_object_id = Class.new(BlankSlate) do
reveal(:object_id)
end
obj1 = with_object_id.new
obj2 = with_object_id.new
assert obj1.object_id != obj2.object_id,
"Revealed methods should not be bound to a particular instance"
end
end

@ -0,0 +1,150 @@
#!/usr/bin/env ruby
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'test/unit'
require 'test/preload'
require 'builder'
require 'builder/xmlevents'
class TestEvents < Test::Unit::TestCase
class Target
attr_reader :events
def initialize
@events = []
end
def start_tag(tag, attrs)
@events << [:start_tag, tag, attrs]
end
def end_tag(tag)
@events << [:end_tag, tag]
end
def text(string)
@events << [:text, string]
end
end
def setup
@target = Target.new
@xml = Builder::XmlEvents.new(:target=>@target)
end
def test_simple
@xml.one
expect [:start_tag, :one, nil]
expect [:end_tag, :one]
expect_done
end
def test_nested
@xml.one { @xml.two }
expect [:start_tag, :one, nil]
expect [:start_tag, :two, nil]
expect [:end_tag, :two]
expect [:end_tag, :one]
expect_done
end
def test_text
@xml.one("a")
expect [:start_tag, :one, nil]
expect [:text, "a"]
expect [:end_tag, :one]
expect_done
end
def test_special_text
@xml.one("H&R")
expect [:start_tag, :one, nil]
expect [:text, "H&R"]
expect [:end_tag, :one]
expect_done
end
def test_text_with_entity
@xml.one("H&amp;R")
expect [:start_tag, :one, nil]
expect [:text, "H&amp;R"]
expect [:end_tag, :one]
expect_done
end
def test_attributes
@xml.a(:b=>"c", :x=>"y")
expect [:start_tag, :a, {:x => "y", :b => "c"}]
expect [:end_tag, :a]
expect_done
end
def test_moderately_complex
@xml.tag! "address-book" do |x|
x.entry :id=>"1" do
x.name {
x.first "Bill"
x.last "Smith"
}
x.address "Cincinnati"
end
x.entry :id=>"2" do
x.name {
x.first "John"
x.last "Doe"
}
x.address "Columbus"
end
end
expect [:start_tag, "address-book".intern, nil]
expect [:start_tag, :entry, {:id => "1"}]
expect [:start_tag, :name, nil]
expect [:start_tag, :first, nil]
expect [:text, "Bill"]
expect [:end_tag, :first]
expect [:start_tag, :last, nil]
expect [:text, "Smith"]
expect [:end_tag, :last]
expect [:end_tag, :name]
expect [:start_tag, :address, nil]
expect [:text, "Cincinnati"]
expect [:end_tag, :address]
expect [:end_tag, :entry]
expect [:start_tag, :entry, {:id => "2"}]
expect [:start_tag, :name, nil]
expect [:start_tag, :first, nil]
expect [:text, "John"]
expect [:end_tag, :first]
expect [:start_tag, :last, nil]
expect [:text, "Doe"]
expect [:end_tag, :last]
expect [:end_tag, :name]
expect [:start_tag, :address, nil]
expect [:text, "Columbus"]
expect [:end_tag, :address]
expect [:end_tag, :entry]
expect [:end_tag, "address-book".intern]
expect_done
end
def expect(value)
assert_equal value, @target.events.shift
end
def expect_done
assert_nil @target.events.shift
end
end

@ -0,0 +1,611 @@
#!/usr/bin/env ruby
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'test/unit'
require 'test/preload'
require 'builder'
require 'builder/xmlmarkup'
class TestMarkup < Test::Unit::TestCase
def setup
@xml = Builder::XmlMarkup.new
end
def test_create
assert_not_nil @xml
end
def test_simple
@xml.simple
assert_equal "<simple/>", @xml.target!
end
def test_value
@xml.value("hi")
assert_equal "<value>hi</value>", @xml.target!
end
def test_empty_value
@xml.value("")
assert_equal "<value></value>", @xml.target!
end
def test_nil_value
@xml.value(nil)
assert_equal "<value/>", @xml.target!
end
def test_no_value
@xml.value()
assert_equal "<value/>", @xml.target!
end
def test_nested
@xml.outer { |x| x.inner("x") }
assert_equal "<outer><inner>x</inner></outer>", @xml.target!
end
def test_attributes
@xml.ref(:id => 12)
assert_equal %{<ref id="12"/>}, @xml.target!
end
def test_single_quotes_for_attrs
@xml = Builder::XmlMarkup.new(:quote => :single)
@xml.ref(:id => 12)
assert_equal %{<ref id='12'/>}, @xml.target!
end
def test_mixed_quotes_for_attrs
@xml = Builder::XmlMarkup.new(:quote => :single)
x = Builder::XmlMarkup.new(:target=>@xml, :quote => :double)
@xml.ref(:id => 12) do
x.link(:id => 13)
end
assert_equal %{<ref id='12'><link id="13"/></ref>}, @xml.target!
end
def test_string_attributes_are_escaped_by_default
@xml.ref(:id => "H&R")
assert_equal %{<ref id="H&amp;R"/>}, @xml.target!
end
def test_symbol_attributes_are_unescaped_by_default
@xml.ref(:id => :"H&amp;R")
assert_equal %{<ref id="H&amp;R"/>}, @xml.target!
end
def test_attributes_escaping_can_be_turned_on
@xml = Builder::XmlMarkup.new
@xml.ref(:id => "<H&R \"block\">")
assert_equal %{<ref id="&lt;H&amp;R &quot;block&quot;&gt;"/>}, @xml.target!
end
def test_mixed_attribute_escaping_with_nested_builders
x = Builder::XmlMarkup.new(:target=>@xml)
@xml.ref(:id=>:"H&amp;R") {
x.element(:tag=>"Long&Short")
}
assert_equal "<ref id=\"H&amp;R\"><element tag=\"Long&amp;Short\"/></ref>",
@xml.target!
end
def test_multiple_attributes
@xml.ref(:id => 12, :name => "bill")
assert_match %r{^<ref( id="12"| name="bill"){2}/>$}, @xml.target!
end
def test_attributes_with_text
@xml.a("link", :href=>"http://onestepback.org")
assert_equal %{<a href="http://onestepback.org">link</a>}, @xml.target!
end
def test_attributes_with_newlines
@xml.abbr("W3C", :title=>"World\nWide\rWeb\r\nConsortium")
assert_equal %{<abbr title="World&#10;Wide&#13;Web&#13;&#10;Consortium">W3C</abbr>},
@xml.target!
end
def test_complex
@xml.body(:bg=>"#ffffff") { |x|
x.title("T", :style=>"red")
}
assert_equal %{<body bg="#ffffff"><title style="red">T</title></body>}, @xml.target!
end
def test_funky_symbol
@xml.tag!("non-ruby-token", :id=>1) { |x| x.ok }
assert_equal %{<non-ruby-token id="1"><ok/></non-ruby-token>}, @xml.target!
end
def test_tag_can_handle_private_method
@xml.tag!("loop", :id=>1) { |x| x.ok }
assert_equal %{<loop id="1"><ok/></loop>}, @xml.target!
end
def test_no_explicit_marker
@xml.p { |x| x.b("HI") }
assert_equal "<p><b>HI</b></p>", @xml.target!
end
def test_reference_local_vars
n = 3
@xml.ol { |x| n.times { x.li(n) } }
assert_equal "<ol><li>3</li><li>3</li><li>3</li></ol>", @xml.target!
end
def test_reference_methods
@xml.title { |x| x.a { x.b(name) } }
assert_equal "<title><a><b>bob</b></a></title>", @xml.target!
end
def test_append_text
@xml.p { |x| x.br; x.text! "HI" }
assert_equal "<p><br/>HI</p>", @xml.target!
end
def test_ambiguous_markup
ex = assert_raise(ArgumentError) {
@xml.h1("data1") { b }
}
assert_match(/\btext\b/, ex.message)
assert_match(/\bblock\b/, ex.message)
end
def test_capitalized_method
@xml.P { |x| x.B("hi"); x.BR(); x.EM { x.text! "world" } }
assert_equal "<P><B>hi</B><BR/><EM>world</EM></P>", @xml.target!
end
def test_escaping
@xml.div { |x| x.text! "<hi>"; x.em("H&R Block") }
assert_equal %{<div>&lt;hi&gt;<em>H&amp;R Block</em></div>}, @xml.target!
end
def test_nil
b = Builder::XmlMarkup.new
b.tag! "foo", nil
assert_equal %{<foo/>}, b.target!
end
def test_nil_without_explicit_nil_handling
b = Builder::XmlMarkup.new(:explicit_nil_handling => false)
b.tag! "foo", nil
assert_equal %{<foo/>}, b.target!
end
def test_nil_with_explicit_nil_handling
b = Builder::XmlMarkup.new(:explicit_nil_handling => true)
b.tag! "foo", nil
assert_equal %{<foo nil="true"/>}, b.target!
end
def test_non_escaping
@xml.div("ns:xml"=>:"&xml;") { |x| x << "<h&i>"; x.em("H&R Block") }
assert_equal %{<div ns:xml="&xml;"><h&i><em>H&amp;R Block</em></div>}, @xml.target!
end
def test_return_value
str = @xml.x("men")
assert_equal @xml.target!, str
end
def test_stacked_builders
b = Builder::XmlMarkup.new( :target => @xml )
b.div { @xml.span { @xml.a("text", :href=>"ref") } }
assert_equal "<div><span><a href=\"ref\">text</a></span></div>", @xml.target!
end
def name
"bob"
end
end
class TestAttributeEscaping < Test::Unit::TestCase
def setup
@xml = Builder::XmlMarkup.new
end
def test_element_gt
@xml.title('1<2')
assert_equal '<title>1&lt;2</title>', @xml.target!
end
def test_element_amp
@xml.title('AT&T')
assert_equal '<title>AT&amp;T</title>', @xml.target!
end
def test_element_amp2
@xml.title('&amp;')
assert_equal '<title>&amp;amp;</title>', @xml.target!
end
def test_attr_less
@xml.a(:title => '2>1')
assert_equal '<a title="2&gt;1"/>', @xml.target!
end
def test_attr_amp
@xml.a(:title => 'AT&T')
assert_equal '<a title="AT&amp;T"/>', @xml.target!
end
def test_attr_quot
@xml.a(:title => '"x"')
assert_equal '<a title="&quot;x&quot;"/>', @xml.target!
end
end
class TestNameSpaces < Test::Unit::TestCase
def setup
@xml = Builder::XmlMarkup.new(:indent=>2)
end
def test_simple_name_spaces
@xml.rdf :RDF
assert_equal "<rdf:RDF/>\n", @xml.target!
end
def test_long
xml = Builder::XmlMarkup.new(:indent=>2)
xml.instruct!
xml.rdf :RDF,
"xmlns:rdf" => :"&rdf;",
"xmlns:rdfs" => :"&rdfs;",
"xmlns:xsd" => :"&xsd;",
"xmlns:owl" => :"&owl;" do
xml.owl :Class, :'rdf:ID'=>'Bird' do
xml.rdfs :label, 'bird'
xml.rdfs :subClassOf do
xml.owl :Restriction do
xml.owl :onProperty, 'rdf:resource'=>'#wingspan'
xml.owl :maxCardinality,1,'rdf:datatype'=>'&xsd;nonNegativeInteger'
end
end
end
end
assert_match(/^<\?xml/, xml.target!)
assert_match(/\n<rdf:RDF/m, xml.target!)
assert_match(/xmlns:rdf="&rdf;"/m, xml.target!)
assert_match(/<owl:Restriction>/m, xml.target!)
end
def test_ensure
xml = Builder::XmlMarkup.new
xml.html do
xml.body do
begin
xml.p do
raise Exception.new('boom')
end
rescue Exception => e
xml.pre e
end
end
end
assert_match %r{<p>}, xml.target!
assert_match %r{</p>}, xml.target!
end
end
class TestDeclarations < Test::Unit::TestCase
def setup
@xml = Builder::XmlMarkup.new(:indent=>2)
end
def test_declare
@xml.declare! :element
assert_equal "<!element>\n", @xml.target!
end
def test_bare_arg
@xml.declare! :element, :arg
assert_equal"<!element arg>\n", @xml.target!
end
def test_string_arg
@xml.declare! :element, "string"
assert_equal"<!element \"string\">\n", @xml.target!
end
def test_mixed_args
@xml.declare! :element, :x, "y", :z, "-//OASIS//DTD DocBook XML//EN"
assert_equal "<!element x \"y\" z \"-//OASIS//DTD DocBook XML//EN\">\n", @xml.target!
end
def test_nested_declarations
@xml = Builder::XmlMarkup.new
@xml.declare! :DOCTYPE, :chapter do |x|
x.declare! :ELEMENT, :chapter, "(title,para+)".intern
end
assert_equal "<!DOCTYPE chapter [<!ELEMENT chapter (title,para+)>]>", @xml.target!
end
def test_nested_indented_declarations
@xml.declare! :DOCTYPE, :chapter do |x|
x.declare! :ELEMENT, :chapter, "(title,para+)".intern
end
assert_equal "<!DOCTYPE chapter [\n <!ELEMENT chapter (title,para+)>\n]>\n", @xml.target!
end
def test_complex_declaration
@xml.declare! :DOCTYPE, :chapter do |x|
x.declare! :ELEMENT, :chapter, "(title,para+)".intern
x.declare! :ELEMENT, :title, "(#PCDATA)".intern
x.declare! :ELEMENT, :para, "(#PCDATA)".intern
end
expected = %{<!DOCTYPE chapter [
<!ELEMENT chapter (title,para+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT para (#PCDATA)>
]>
}
assert_equal expected, @xml.target!
end
end
class TestSpecialMarkup < Test::Unit::TestCase
def setup
@xml = Builder::XmlMarkup.new(:indent=>2)
end
def test_comment
@xml.comment!("COMMENT")
assert_equal "<!-- COMMENT -->\n", @xml.target!
end
def test_indented_comment
@xml.p { @xml.comment! "OK" }
assert_equal "<p>\n <!-- OK -->\n</p>\n", @xml.target!
end
def test_instruct
@xml.instruct! :abc, :version=>"0.9"
assert_equal "<?abc version=\"0.9\"?>\n", @xml.target!
end
def test_indented_instruct
@xml.p { @xml.instruct! :xml }
assert_match %r{<p>\n <\?xml version="1.0" encoding="UTF-8"\?>\n</p>\n},
@xml.target!
end
def test_instruct_without_attributes
@xml.instruct! :zz
assert_equal "<?zz?>\n", @xml.target!
end
def test_xml_instruct
@xml.instruct!
assert_match(/^<\?xml version="1.0" encoding="UTF-8"\?>$/, @xml.target!)
end
def test_xml_instruct_with_overrides
@xml.instruct! :xml, :encoding=>"UCS-2"
assert_match(/^<\?xml version="1.0" encoding="UCS-2"\?>$/, @xml.target!)
end
def test_xml_instruct_with_standalong
@xml.instruct! :xml, :encoding=>"UCS-2", :standalone=>"yes"
assert_match(/^<\?xml version="1.0" encoding="UCS-2" standalone="yes"\?>$/, @xml.target!)
end
def test_no_blocks
assert_raise(Builder::IllegalBlockError) do
@xml.instruct! { |x| x.hi }
end
assert_raise(Builder::IllegalBlockError) do
@xml.comment!(:element) { |x| x.hi }
end
end
def test_cdata
@xml.cdata!("TEST")
assert_equal "<![CDATA[TEST]]>\n", @xml.target!
end
def test_cdata_with_ampersand
@xml.cdata!("TEST&CHECK")
assert_equal "<![CDATA[TEST&CHECK]]>\n", @xml.target!
end
def test_cdata_with_included_close
@xml.cdata!("TEST]]>CHECK")
assert_equal "<![CDATA[TEST]]]]><![CDATA[>CHECK]]>\n", @xml.target!
end
end
class TestIndentedXmlMarkup < Test::Unit::TestCase
def setup
@xml = Builder::XmlMarkup.new(:indent=>2)
end
def test_one_level
@xml.ol { |x| x.li "text" }
assert_equal "<ol>\n <li>text</li>\n</ol>\n", @xml.target!
end
def test_two_levels
@xml.p { |x|
x.ol { x.li "text" }
x.br
}
assert_equal "<p>\n <ol>\n <li>text</li>\n </ol>\n <br/>\n</p>\n", @xml.target!
end
def test_initial_level
@xml = Builder::XmlMarkup.new(:indent=>2, :margin=>4)
@xml.name { |x| x.first("Jim") }
assert_equal " <name>\n <first>Jim</first>\n </name>\n", @xml.target!
end
class TestUtfMarkup < Test::Unit::TestCase
if ! String.method_defined?(:encode)
def setup
@old_kcode = $KCODE
end
def teardown
$KCODE = @old_kcode
end
def test_use_entities_if_no_encoding_is_given_and_kcode_is_none
$KCODE = 'NONE'
xml = Builder::XmlMarkup.new
xml.p("\xE2\x80\x99")
assert_match(%r(<p>&#8217;</p>), xml.target!) #
end
def test_use_entities_if_encoding_is_utf_but_kcode_is_not
$KCODE = 'NONE'
xml = Builder::XmlMarkup.new
xml.instruct!(:xml, :encoding => 'UTF-8')
xml.p("\xE2\x80\x99")
assert_match(%r(<p>&#8217;</p>), xml.target!) #
end
else
# change in behavior. As there is no $KCODE anymore, the default
# moves from "does not understand utf-8" to "supports utf-8".
def test_use_entities_if_no_encoding_is_given_and_kcode_is_none
xml = Builder::XmlMarkup.new
xml.p("\xE2\x80\x99")
assert_match("<p>\u2019</p>", xml.target!) #
end
def test_use_entities_if_encoding_is_utf_but_kcode_is_not
xml = Builder::XmlMarkup.new
xml.instruct!(:xml, :encoding => 'UTF-8')
xml.p("\xE2\x80\x99")
assert_match("<p>\u2019</p>", xml.target!) #
end
end
def encode string, encoding
if !String.method_defined?(:encode)
$KCODE = encoding
string
elsif encoding == 'UTF8'
string.force_encoding('UTF-8')
else
string
end
end
def test_use_entities_if_kcode_is_utf_but_encoding_is_dummy_encoding
xml = Builder::XmlMarkup.new
xml.instruct!(:xml, :encoding => 'UTF-16')
xml.p(encode("\xE2\x80\x99", 'UTF8'))
assert_match(%r(<p>&#8217;</p>), xml.target!) #
end
def test_use_entities_if_kcode_is_utf_but_encoding_is_unsupported_encoding
xml = Builder::XmlMarkup.new
xml.instruct!(:xml, :encoding => 'UCS-2')
xml.p(encode("\xE2\x80\x99", 'UTF8'))
assert_match(%r(<p>&#8217;</p>), xml.target!) #
end
def test_use_utf8_if_encoding_defaults_and_kcode_is_utf8
xml = Builder::XmlMarkup.new
xml.p(encode("\xE2\x80\x99",'UTF8'))
assert_equal encode("<p>\xE2\x80\x99</p>",'UTF8'), xml.target!
end
def test_use_utf8_if_both_encoding_and_kcode_are_utf8
xml = Builder::XmlMarkup.new
xml.instruct!(:xml, :encoding => 'UTF-8')
xml.p(encode("\xE2\x80\x99",'UTF8'))
assert_match encode("<p>\xE2\x80\x99</p>",'UTF8'), xml.target!
end
def test_use_utf8_if_both_encoding_and_kcode_are_utf8_with_lowercase
xml = Builder::XmlMarkup.new
xml.instruct!(:xml, :encoding => 'utf-8')
xml.p(encode("\xE2\x80\x99",'UTF8'))
assert_match encode("<p>\xE2\x80\x99</p>",'UTF8'), xml.target!
end
end
class TestXmlEvents < Test::Unit::TestCase
def setup
@handler = EventHandler.new
@xe = Builder::XmlEvents.new(:target=>@handler)
end
def test_simple
@xe.p
assert_equal [:start, :p, nil], @handler.events.shift
assert_equal [:end, :p], @handler.events.shift
end
def test_text
@xe.p("HI")
assert_equal [:start, :p, nil], @handler.events.shift
assert_equal [:text, "HI"], @handler.events.shift
assert_equal [:end, :p], @handler.events.shift
end
def test_attributes
@xe.p("id"=>"2")
ev = @handler.events.shift
assert_equal [:start, :p], ev[0,2]
assert_equal "2", ev[2]['id']
assert_equal [:end, :p], @handler.events.shift
end
def test_indented
@xml = Builder::XmlEvents.new(:indent=>2, :target=>@handler)
@xml.p { |x| x.b("HI") }
assert_equal [:start, :p, nil], @handler.events.shift
assert_equal "\n ", pop_text
assert_equal [:start, :b, nil], @handler.events.shift
assert_equal "HI", pop_text
assert_equal [:end, :b], @handler.events.shift
assert_equal "\n", pop_text
assert_equal [:end, :p], @handler.events.shift
end
def pop_text
result = ''
while ! @handler.events.empty? && @handler.events[0][0] == :text
result << @handler.events[0][1]
@handler.events.shift
end
result
end
class EventHandler
attr_reader :events
def initialize
@events = []
end
def start_tag(sym, attrs)
@events << [:start, sym, attrs]
end
def end_tag(sym)
@events << [:end, sym]
end
def text(txt)
@events << [:text, txt]
end
end
end
end

@ -0,0 +1,62 @@
#!/usr/bin/env ruby
#--
# Portions copyright 2011 by Bart ten Brinke (info@retrosync.com).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'test/unit'
require 'test/preload'
require 'builder'
class TestMethodCaching < Test::Unit::TestCase
# We can directly ask if xml object responds to the cache_me or
# do_not_cache_me methods because xml is derived from BasicObject
# (and repond_to? is not defined in BasicObject).
#
# Instead we are going to stub out method_missing so that it throws
# an error, and then make sure that error is either thrown or not
# thrown as appropriate.
def teardown
super
Builder::XmlBase.cache_method_calls = true
end
def test_caching_does_not_break_weird_symbols
xml = Builder::XmlMarkup.new
xml.__send__("work-order", 1)
assert_equal "<work-order>1</work-order>", xml.target!
end
def test_method_call_caching
xml = Builder::XmlMarkup.new
xml.cache_me
def xml.method_missing(*args)
::Kernel.fail StandardError, "SHOULD NOT BE CALLED"
end
assert_nothing_raised do
xml.cache_me
end
end
def test_method_call_caching_disabled
Builder::XmlBase.cache_method_calls = false
xml = Builder::XmlMarkup.new
xml.do_not_cache_me
def xml.method_missing(*args)
::Kernel.fail StandardError, "SHOULD BE CALLED"
end
assert_raise(StandardError, "SHOULD BE CALLED") do
xml.do_not_cache_me
end
end
end

@ -0,0 +1,39 @@
#!/usr/bin/env ruby
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
require 'test/unit'
require 'builder/xchar'
class TestNameCollisions < Test::Unit::TestCase
module Collide
def xchr
end
end
def test_no_collision
assert_nothing_raised do
Builder.check_for_name_collision(Collide, :not_defined)
end
end
def test_collision
assert_raise RuntimeError do
Builder.check_for_name_collision(Collide, "xchr")
end
end
def test_collision_with_symbol
assert_raise RuntimeError do
Builder.check_for_name_collision(Collide, :xchr)
end
end
end

@ -0,0 +1,78 @@
#!/usr/bin/env ruby
# encoding: us-ascii
#--
# Portions copyright 2004 by Jim Weirich (jim@weirichhouse.org).
# Portions copyright 2005 by Sam Ruby (rubys@intertwingly.net).
# All rights reserved.
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#++
#!/usr/bin/env ruby
require 'test/unit'
require 'builder/xchar'
if String.method_defined?(:encode)
class String
ENCODING_BINARY = Encoding.find('BINARY')
# shim method for testing purposes
def to_xs(escape=true)
raise NameError.new('to_xs') unless caller[0].index(__FILE__)
result = Builder::XChar.encode(self)
if escape
result.gsub(/[^\u0000-\u007F]/) {|c| "&##{c.ord};"}
else
# really only useful for testing purposes
result.force_encoding(ENCODING_BINARY)
end
end
end
end
class TestXmlEscaping < Test::Unit::TestCase
REPLACEMENT_CHAR = Builder::XChar::REPLACEMENT_CHAR.to_xs
def test_ascii
assert_equal 'abc', 'abc'.to_xs
end
def test_predefined
assert_equal '&amp;', '&'.to_xs # ampersand
assert_equal '&lt;', '<'.to_xs # left angle bracket
assert_equal '&gt;', '>'.to_xs # right angle bracket
end
def test_invalid
assert_equal REPLACEMENT_CHAR, "\x00".to_xs # null
assert_equal REPLACEMENT_CHAR, "\x0C".to_xs # form feed
assert_equal REPLACEMENT_CHAR, "\xEF\xBF\xBF".to_xs # U+FFFF
end
def test_iso_8859_1
assert_equal '&#231;', "\xE7".to_xs # small c cedilla
assert_equal '&#169;', "\xA9".to_xs # copyright symbol
end
def test_win_1252
assert_equal '&#8217;', "\x92".to_xs # smart quote
assert_equal '&#8364;', "\x80".to_xs # euro
end
def test_utf8
assert_equal '&#8217;', "\xE2\x80\x99".to_xs # right single quote
assert_equal '&#169;', "\xC2\xA9".to_xs # copy
end
def test_utf8_verbatim
assert_equal "\xE2\x80\x99", "\xE2\x80\x99".to_xs(false) # right single quote
assert_equal "\xC2\xA9", "\xC2\xA9".to_xs(false) # copy
assert_equal "\xC2\xA9&amp;\xC2\xA9",
"\xC2\xA9&\xC2\xA9".to_xs(false) # copy with ampersand
end
end

@ -0,0 +1,20 @@
Copyright (c) 2009-2015 Rick Olson, Zack Hobson
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,245 @@
# Faraday
Faraday is an HTTP client lib that provides a common interface over many
adapters (such as Net::HTTP) and embraces the concept of Rack middleware when
processing the request/response cycle.
Faraday supports these adapters:
* [Net::HTTP][net_http] _(default)_
* [Net::HTTP::Persistent][persistent]
* [Excon][]
* [Typhoeus][]
* [Patron][]
* [EventMachine][]
* [HTTPClient][]
It also includes a Rack adapter for hitting loaded Rack applications through
Rack::Test, and a Test adapter for stubbing requests by hand.
## Usage
```ruby
conn = Faraday.new(:url => 'http://sushi.com') do |faraday|
faraday.request :url_encoded # form-encode POST params
faraday.response :logger # log requests to STDOUT
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
end
## GET ##
response = conn.get '/nigiri/sake.json' # GET http://sushi.com/nigiri/sake.json
response.body
conn.get '/nigiri', { :name => 'Maguro' } # GET http://sushi.com/nigiri?name=Maguro
conn.get do |req| # GET http://sushi.com/search?page=2&limit=100
req.url '/search', :page => 2
req.params['limit'] = 100
end
## POST ##
conn.post '/nigiri', { :name => 'Maguro' } # POST "name=maguro" to http://sushi.com/nigiri
# post payload as JSON instead of "www-form-urlencoded" encoding:
conn.post do |req|
req.url '/nigiri'
req.headers['Content-Type'] = 'application/json'
req.body = '{ "name": "Unagi" }'
end
## Per-request options ##
conn.get do |req|
req.url '/search'
req.options.timeout = 5 # open/read timeout in seconds
req.options.open_timeout = 2 # connection open timeout in seconds
end
```
If you don't need to set up anything, you can roll with just the default middleware
stack and default adapter (see [Faraday::RackBuilder#initialize](https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb)):
```ruby
response = Faraday.get 'http://sushi.com/nigiri/sake.json'
```
### Changing how parameters are serialized
Sometimes you need to send the same URL parameter multiple times with different
values. This requires manually setting the parameter encoder and can be done on
either per-connection or per-request basis.
```ruby
# per-connection setting
conn = Faraday.new :params_encoder => Faraday::FlatParamsEncoder
conn.get do |req|
# per-request setting:
# req.options.params_encoder = my_encoder
req.params['roll'] = ['california', 'philadelphia']
end
# GET 'http://sushi.com?roll=california&roll=philadelphia'
```
The value of Faraday `params_encoder` can be any object that responds to:
* `encode(hash) #=> String`
* `decode(string) #=> Hash`
The encoder will affect both how query strings are processed and how POST bodies
get serialized. The default encoder is Faraday::NestedParamsEncoder.
## Advanced middleware usage
The order in which middleware is stacked is important. Like with Rack, the
first middleware on the list wraps all others, while the last middleware is the
innermost one, so that must be the adapter.
```ruby
Faraday.new(...) do |conn|
# POST/PUT params encoders:
conn.request :multipart
conn.request :url_encoded
conn.adapter :net_http
end
```
This request middleware setup affects POST/PUT requests in the following way:
1. `Request::Multipart` checks for files in the payload, otherwise leaves
everything untouched;
2. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not
already encoded or of another type
Swapping middleware means giving the other priority. Specifying the
"Content-Type" for the request is explicitly stating which middleware should
process it.
Examples:
```ruby
# uploading a file:
payload[:profile_pic] = Faraday::UploadIO.new('/path/to/avatar.jpg', 'image/jpeg')
# "Multipart" middleware detects files and encodes with "multipart/form-data":
conn.put '/profile', payload
```
## Writing middleware
Middleware are classes that implement a `call` instance method. They hook into
the request/response cycle.
```ruby
def call(request_env)
# do something with the request
# request_env[:request_headers].merge!(...)
@app.call(request_env).on_complete do |response_env|
# do something with the response
# response_env[:response_headers].merge!(...)
end
end
```
It's important to do all processing of the response only in the `on_complete`
block. This enables middleware to work in parallel mode where requests are
asynchronous.
The `env` is a hash with symbol keys that contains info about the request and,
later, response. Some keys are:
```
# request phase
:method - :get, :post, ...
:url - URI for the current request; also contains GET parameters
:body - POST parameters for :post/:put requests
:request_headers
# response phase
:status - HTTP response status code, such as 200
:body - the response body
:response_headers
```
## Using Faraday for testing
```ruby
# It's possible to define stubbed request outside a test adapter block.
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get('/tamago') { |env| [200, {}, 'egg'] }
end
# You can pass stubbed request to the test adapter or define them in a block
# or a combination of the two.
test = Faraday.new do |builder|
builder.adapter :test, stubs do |stub|
stub.get('/ebi') { |env| [ 200, {}, 'shrimp' ]}
end
end
# It's also possible to stub additional requests after the connection has
# been initialized. This is useful for testing.
stubs.get('/uni') { |env| [ 200, {}, 'urchin' ]}
resp = test.get '/tamago'
resp.body # => 'egg'
resp = test.get '/ebi'
resp.body # => 'shrimp'
resp = test.get '/uni'
resp.body # => 'urchin'
resp = test.get '/else' #=> raises "no such stub" error
# If you like, you can treat your stubs as mocks by verifying that all of
# the stubbed calls were made. NOTE that this feature is still fairly
# experimental: It will not verify the order or count of any stub, only that
# it was called once during the course of the test.
stubs.verify_stubbed_calls
```
## TODO
* support streaming requests/responses
* better stubbing API
## Supported Ruby versions
This library aims to support and is [tested against][travis] the following Ruby
implementations:
* Ruby 1.8.7+
* [JRuby][] 1.7+
* [Rubinius][] 2+
If something doesn't work on one of these Ruby versions, it's a bug.
This library may inadvertently work (or seem to work) on other Ruby
implementations, however support will only be provided for the versions listed
above.
If you would like this library to support another Ruby version, you may
volunteer to be a maintainer. Being a maintainer entails making sure all tests
run and pass on that implementation. When something breaks on your
implementation, you will be responsible for providing patches in a timely
fashion. If critical issues for a particular implementation exist at the time
of a major release, support for that Ruby version may be dropped.
## Copyright
Copyright (c) 2009-2013 [Rick Olson](mailto:technoweenie@gmail.com), Zack Hobson.
See [LICENSE][] for details.
[net_http]: http://ruby-doc.org/stdlib/libdoc/net/http/rdoc/Net/HTTP.html
[persistent]: https://github.com/drbrain/net-http-persistent
[travis]: http://travis-ci.org/lostisland/faraday
[excon]: https://github.com/geemus/excon#readme
[typhoeus]: https://github.com/typhoeus/typhoeus#readme
[patron]: http://toland.github.com/patron/
[eventmachine]: https://github.com/igrigorik/em-http-request#readme
[httpclient]: https://github.com/nahi/httpclient
[jruby]: http://jruby.org/
[rubinius]: http://rubini.us/
[license]: LICENSE.md

@ -0,0 +1,268 @@
require 'thread'
require 'cgi'
require 'set'
require 'forwardable'
# Public: This is the main namespace for Faraday. You can either use it to
# create Faraday::Connection objects, or access it directly.
#
# Examples
#
# Faraday.get "http://faraday.com"
#
# conn = Faraday.new "http://faraday.com"
# conn.get '/'
#
module Faraday
VERSION = "0.9.2"
class << self
# Public: Gets or sets the root path that Faraday is being loaded from.
# This is the root from where the libraries are auto-loaded from.
attr_accessor :root_path
# Public: Gets or sets the path that the Faraday libs are loaded from.
attr_accessor :lib_path
# Public: Gets or sets the Symbol key identifying a default Adapter to use
# for the default Faraday::Connection.
attr_reader :default_adapter
# Public: Sets the default Faraday::Connection for simple scripts that
# access the Faraday constant directly.
#
# Faraday.get "https://faraday.com"
attr_writer :default_connection
# Public: Sets the default options used when calling Faraday#new.
attr_writer :default_connection_options
# Public: Initializes a new Faraday::Connection.
#
# url - The optional String base URL to use as a prefix for all
# requests. Can also be the options Hash.
# options - The optional Hash used to configure this Faraday::Connection.
# Any of these values will be set on every request made, unless
# overridden for a specific request.
# :url - String base URL.
# :params - Hash of URI query unencoded key/value pairs.
# :headers - Hash of unencoded HTTP header key/value pairs.
# :request - Hash of request options.
# :ssl - Hash of SSL options.
# :proxy - Hash of Proxy options.
#
# Examples
#
# Faraday.new 'http://faraday.com'
#
# # http://faraday.com?page=1
# Faraday.new 'http://faraday.com', :params => {:page => 1}
#
# # same
#
# Faraday.new :url => 'http://faraday.com',
# :params => {:page => 1}
#
# Returns a Faraday::Connection.
def new(url = nil, options = nil)
block = block_given? ? Proc.new : nil
options = options ? default_connection_options.merge(options) : default_connection_options.dup
Faraday::Connection.new(url, options, &block)
end
# Internal: Requires internal Faraday libraries.
#
# *libs - One or more relative String names to Faraday classes.
#
# Returns nothing.
def require_libs(*libs)
libs.each do |lib|
require "#{lib_path}/#{lib}"
end
end
# Public: Updates default adapter while resetting
# #default_connection.
#
# Returns the new default_adapter.
def default_adapter=(adapter)
@default_connection = nil
@default_adapter = adapter
end
alias require_lib require_libs
private
# Internal: Proxies method calls on the Faraday constant to
# #default_connection.
def method_missing(name, *args, &block)
default_connection.send(name, *args, &block)
end
end
self.root_path = File.expand_path "..", __FILE__
self.lib_path = File.expand_path "../faraday", __FILE__
self.default_adapter = :net_http
# Gets the default connection used for simple scripts.
#
# Returns a Faraday::Connection, configured with the #default_adapter.
def self.default_connection
@default_connection ||= Connection.new
end
# Gets the default connection options used when calling Faraday#new.
#
# Returns a Faraday::ConnectionOptions.
def self.default_connection_options
@default_connection_options ||= ConnectionOptions.new
end
if (!defined?(RUBY_ENGINE) || "ruby" == RUBY_ENGINE) && RUBY_VERSION < '1.9'
begin
require 'system_timer'
Timer = SystemTimer
rescue LoadError
warn "Faraday: you may want to install system_timer for reliable timeouts"
end
end
unless const_defined? :Timer
require 'timeout'
Timer = Timeout
end
# Public: Adds the ability for other modules to register and lookup
# middleware classes.
module MiddlewareRegistry
# Public: Register middleware class(es) on the current module.
#
# mapping - A Hash mapping Symbol keys to classes. Classes can be expressed
# as fully qualified constant, or a Proc that will be lazily
# called to return the former.
#
# Examples
#
# module Faraday
# class Whatever
# # Middleware looked up by :foo returns Faraday::Whatever::Foo.
# register_middleware :foo => Foo
#
# # Middleware looked up by :bar returns Faraday::Whatever.const_get(:Bar)
# register_middleware :bar => :Bar
#
# # Middleware looked up by :baz requires 'baz' and returns Faraday::Whatever.const_get(:Baz)
# register_middleware :baz => [:Baz, 'baz']
# end
# end
#
# Returns nothing.
def register_middleware(autoload_path = nil, mapping = nil)
if mapping.nil?
mapping = autoload_path
autoload_path = nil
end
middleware_mutex do
@middleware_autoload_path = autoload_path if autoload_path
(@registered_middleware ||= {}).update(mapping)
end
end
# Public: Lookup middleware class with a registered Symbol shortcut.
#
# key - The Symbol key for the registered middleware.
#
# Examples
#
# module Faraday
# class Whatever
# register_middleware :foo => Foo
# end
# end
#
# Faraday::Whatever.lookup_middleware(:foo)
# # => Faraday::Whatever::Foo
#
# Returns a middleware Class.
def lookup_middleware(key)
load_middleware(key) ||
raise(Faraday::Error.new("#{key.inspect} is not registered on #{self}"))
end
def middleware_mutex(&block)
@middleware_mutex ||= begin
require 'monitor'
Monitor.new
end
@middleware_mutex.synchronize(&block)
end
def fetch_middleware(key)
defined?(@registered_middleware) && @registered_middleware[key]
end
def load_middleware(key)
value = fetch_middleware(key)
case value
when Module
value
when Symbol, String
middleware_mutex do
@registered_middleware[key] = const_get(value)
end
when Proc
middleware_mutex do
@registered_middleware[key] = value.call
end
when Array
middleware_mutex do
const, path = value
if root = @middleware_autoload_path
path = "#{root}/#{path}"
end
require(path)
@registered_middleware[key] = const
end
load_middleware(key)
end
end
end
def self.const_missing(name)
if name.to_sym == :Builder
warn "Faraday::Builder is now Faraday::RackBuilder."
const_set name, RackBuilder
else
super
end
end
require_libs "utils", "options", "connection", "rack_builder", "parameters",
"middleware", "adapter", "request", "response", "upload_io", "error"
if !ENV["FARADAY_NO_AUTOLOAD"]
require_lib 'autoload'
end
end
# not pulling in active-support JUST for this method. And I love this method.
class Object
# The primary purpose of this method is to "tap into" a method chain,
# in order to perform operations on intermediate results within the chain.
#
# Examples
#
# (1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
# tap { |x| puts "array: #{x.inspect}" }.
# select { |x| x%2 == 0 }.
# tap { |x| puts "evens: #{x.inspect}" }.
# map { |x| x*x }.
# tap { |x| puts "squares: #{x.inspect}" }
#
# Yields self.
# Returns self.
def tap
yield(self)
self
end unless Object.respond_to?(:tap)
end

@ -0,0 +1,46 @@
module Faraday
# Public: This is a base class for all Faraday adapters. Adapters are
# responsible for fulfilling a Faraday request.
class Adapter < Middleware
CONTENT_LENGTH = 'Content-Length'.freeze
register_middleware File.expand_path('../adapter', __FILE__),
:test => [:Test, 'test'],
:net_http => [:NetHttp, 'net_http'],
:net_http_persistent => [:NetHttpPersistent, 'net_http_persistent'],
:typhoeus => [:Typhoeus, 'typhoeus'],
:patron => [:Patron, 'patron'],
:em_synchrony => [:EMSynchrony, 'em_synchrony'],
:em_http => [:EMHttp, 'em_http'],
:excon => [:Excon, 'excon'],
:rack => [:Rack, 'rack'],
:httpclient => [:HTTPClient, 'httpclient']
# Public: This module marks an Adapter as supporting parallel requests.
module Parallelism
attr_writer :supports_parallel
def supports_parallel?() @supports_parallel end
def inherited(subclass)
super
subclass.supports_parallel = self.supports_parallel?
end
end
extend Parallelism
self.supports_parallel = false
def call(env)
env.clear_body if env.needs_body?
end
def save_response(env, status, body, headers = nil)
env.status = status
env.body = body
env.response_headers = Utils::Headers.new.tap do |response_headers|
response_headers.update headers unless headers.nil?
yield(response_headers) if block_given?
end
end
end
end

@ -0,0 +1,237 @@
module Faraday
class Adapter
# EventMachine adapter is useful for either asynchronous requests
# when in EM reactor loop or for making parallel requests in
# synchronous code.
class EMHttp < Faraday::Adapter
module Options
def connection_config(env)
options = {}
configure_proxy(options, env)
configure_timeout(options, env)
configure_socket(options, env)
configure_ssl(options, env)
options
end
def request_config(env)
options = {
:body => read_body(env),
:head => env[:request_headers],
# :keepalive => true,
# :file => 'path/to/file', # stream data off disk
}
configure_compression(options, env)
options
end
def read_body(env)
body = env[:body]
body.respond_to?(:read) ? body.read : body
end
def configure_proxy(options, env)
if proxy = request_options(env)[:proxy]
options[:proxy] = {
:host => proxy[:uri].host,
:port => proxy[:uri].port,
:authorization => [proxy[:user], proxy[:password]]
}
end
end
def configure_socket(options, env)
if bind = request_options(env)[:bind]
options[:bind] = {
:host => bind[:host],
:port => bind[:port]
}
end
end
def configure_ssl(options, env)
if env[:url].scheme == 'https' && env[:ssl]
options[:ssl] = {
:cert_chain_file => env[:ssl][:ca_file],
:verify_peer => env[:ssl].fetch(:verify, true)
}
end
end
def configure_timeout(options, env)
timeout, open_timeout = request_options(env).values_at(:timeout, :open_timeout)
options[:connect_timeout] = options[:inactivity_timeout] = timeout
options[:connect_timeout] = open_timeout if open_timeout
end
def configure_compression(options, env)
if env[:method] == :get and not options[:head].key? 'accept-encoding'
options[:head]['accept-encoding'] = 'gzip, compressed'
end
end
def request_options(env)
env[:request]
end
end
include Options
dependency 'em-http'
self.supports_parallel = true
def self.setup_parallel_manager(options = nil)
Manager.new
end
def call(env)
super
perform_request env
@app.call env
end
def perform_request(env)
if parallel?(env)
manager = env[:parallel_manager]
manager.add {
perform_single_request(env).
callback { env[:response].finish(env) }
}
else
unless EventMachine.reactor_running?
error = nil
# start EM, block until request is completed
EventMachine.run do
perform_single_request(env).
callback { EventMachine.stop }.
errback { |client|
error = error_message(client)
EventMachine.stop
}
end
raise_error(error) if error
else
# EM is running: instruct upstream that this is an async request
env[:parallel_manager] = true
perform_single_request(env).
callback { env[:response].finish(env) }.
errback {
# TODO: no way to communicate the error in async mode
raise NotImplementedError
}
end
end
rescue EventMachine::Connectify::CONNECTError => err
if err.message.include?("Proxy Authentication Required")
raise Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
else
raise Error::ConnectionFailed, err
end
rescue => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise Faraday::SSLError, err
else
raise
end
end
# TODO: reuse the connection to support pipelining
def perform_single_request(env)
req = EventMachine::HttpRequest.new(env[:url], connection_config(env))
req.setup_request(env[:method], request_config(env)).callback { |client|
save_response(env, client.response_header.status, client.response) do |resp_headers|
client.response_header.each do |name, value|
resp_headers[name.to_sym] = value
end
end
}
end
def error_message(client)
client.error or "request failed"
end
def raise_error(msg)
errklass = Faraday::Error::ClientError
if msg == Errno::ETIMEDOUT
errklass = Faraday::Error::TimeoutError
msg = "request timed out"
elsif msg == Errno::ECONNREFUSED
errklass = Faraday::Error::ConnectionFailed
msg = "connection refused"
elsif msg == "connection closed by server"
errklass = Faraday::Error::ConnectionFailed
end
raise errklass, msg
end
def parallel?(env)
!!env[:parallel_manager]
end
# The parallel manager is designed to start an EventMachine loop
# and block until all registered requests have been completed.
class Manager
def initialize
reset
end
def reset
@registered_procs = []
@num_registered = 0
@num_succeeded = 0
@errors = []
@running = false
end
def running?() @running end
def add
if running?
perform_request { yield }
else
@registered_procs << Proc.new
end
@num_registered += 1
end
def run
if @num_registered > 0
@running = true
EventMachine.run do
@registered_procs.each do |proc|
perform_request(&proc)
end
end
if @errors.size > 0
raise Faraday::Error::ClientError, @errors.first || "connection failed"
end
end
ensure
reset
end
def perform_request
client = yield
client.callback { @num_succeeded += 1; check_finished }
client.errback { @errors << client.error; check_finished }
end
def check_finished
if @num_succeeded + @errors.size == @num_registered
EventMachine.stop
end
end
end
end
end
end
begin
require 'openssl'
rescue LoadError
warn "Warning: no such file to load -- openssl. Make sure it is installed if you want HTTPS support"
else
require 'faraday/adapter/em_http_ssl_patch'
end if Faraday::Adapter::EMHttp.loaded?

@ -0,0 +1,56 @@
require 'openssl'
require 'em-http'
module EmHttpSslPatch
def ssl_verify_peer(cert_string)
cert = nil
begin
cert = OpenSSL::X509::Certificate.new(cert_string)
rescue OpenSSL::X509::CertificateError
return false
end
@last_seen_cert = cert
if certificate_store.verify(@last_seen_cert)
begin
certificate_store.add_cert(@last_seen_cert)
rescue OpenSSL::X509::StoreError => e
raise e unless e.message == 'cert already in hash table'
end
true
else
raise OpenSSL::SSL::SSLError.new(%(unable to verify the server certificate for "#{host}"))
end
end
def ssl_handshake_completed
return true unless verify_peer?
unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, host)
raise OpenSSL::SSL::SSLError.new(%(host "#{host}" does not match the server certificate))
else
true
end
end
def verify_peer?
parent.connopts.tls[:verify_peer]
end
def host
parent.connopts.host
end
def certificate_store
@certificate_store ||= begin
store = OpenSSL::X509::Store.new
store.set_default_paths
ca_file = parent.connopts.tls[:cert_chain_file]
store.add_file(ca_file) if ca_file
store
end
end
end
EventMachine::HttpStubConnection.send(:include, EmHttpSslPatch)

@ -0,0 +1,100 @@
require 'uri'
module Faraday
class Adapter
class EMSynchrony < Faraday::Adapter
include EMHttp::Options
dependency do
require 'em-synchrony/em-http'
require 'em-synchrony/em-multi'
require 'fiber'
end
self.supports_parallel = true
def self.setup_parallel_manager(options = {})
ParallelManager.new
end
def call(env)
super
request = EventMachine::HttpRequest.new(Utils::URI(env[:url].to_s), connection_config(env))
http_method = env[:method].to_s.downcase.to_sym
# Queue requests for parallel execution.
if env[:parallel_manager]
env[:parallel_manager].add(request, http_method, request_config(env)) do |resp|
save_response(env, resp.response_header.status, resp.response) do |resp_headers|
resp.response_header.each do |name, value|
resp_headers[name.to_sym] = value
end
end
# Finalize the response object with values from `env`.
env[:response].finish(env)
end
# Execute single request.
else
client = nil
block = lambda { request.send(http_method, request_config(env)) }
if !EM.reactor_running?
EM.run do
Fiber.new {
client = block.call
EM.stop
}.resume
end
else
client = block.call
end
raise client.error if client.error
save_response(env, client.response_header.status, client.response) do |resp_headers|
client.response_header.each do |name, value|
resp_headers[name.to_sym] = value
end
end
end
@app.call env
rescue Errno::ECONNREFUSED
raise Error::ConnectionFailed, $!
rescue EventMachine::Connectify::CONNECTError => err
if err.message.include?("Proxy Authentication Required")
raise Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
else
raise Error::ConnectionFailed, err
end
rescue Errno::ETIMEDOUT => err
raise Error::TimeoutError, err
rescue RuntimeError => err
if err.message == "connection closed by server"
raise Error::ConnectionFailed, err
else
raise
end
rescue => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise Faraday::SSLError, err
else
raise
end
end
end
end
end
require 'faraday/adapter/em_synchrony/parallel_manager'
begin
require 'openssl'
rescue LoadError
warn "Warning: no such file to load -- openssl. Make sure it is installed if you want HTTPS support"
else
require 'faraday/adapter/em_http_ssl_patch'
end if Faraday::Adapter::EMSynchrony.loaded?

@ -0,0 +1,66 @@
module Faraday
class Adapter
class EMSynchrony < Faraday::Adapter
class ParallelManager
# Add requests to queue. The `request` argument should be a
# `EM::HttpRequest` object.
def add(request, method, *args, &block)
queue << {
:request => request,
:method => method,
:args => args,
:block => block
}
end
# Run all requests on queue with `EM::Synchrony::Multi`, wrapping
# it in a reactor and fiber if needed.
def run
result = nil
if !EM.reactor_running?
EM.run {
Fiber.new do
result = perform
EM.stop
end.resume
}
else
result = perform
end
result
end
private
# The request queue.
def queue
@queue ||= []
end
# Main `EM::Synchrony::Multi` performer.
def perform
multi = ::EM::Synchrony::Multi.new
queue.each do |item|
method = "a#{item[:method]}".to_sym
req = item[:request].send(method, *item[:args])
req.callback(&item[:block])
req_name = "req_#{multi.requests.size}".to_sym
multi.add(req_name, req)
end
# Clear the queue, so parallel manager objects can be reused.
@queue = []
# Block fiber until all requests have returned.
multi.perform
end
end # ParallelManager
end # EMSynchrony
end # Adapter
end # Faraday

@ -0,0 +1,81 @@
module Faraday
class Adapter
class Excon < Faraday::Adapter
dependency 'excon'
def initialize(app, connection_options = {})
@connection_options = connection_options
super(app)
end
def call(env)
super
opts = {}
if env[:url].scheme == 'https' && ssl = env[:ssl]
opts[:ssl_verify_peer] = !!ssl.fetch(:verify, true)
opts[:ssl_ca_path] = ssl[:ca_path] if ssl[:ca_path]
opts[:ssl_ca_file] = ssl[:ca_file] if ssl[:ca_file]
opts[:client_cert] = ssl[:client_cert] if ssl[:client_cert]
opts[:client_key] = ssl[:client_key] if ssl[:client_key]
opts[:certificate] = ssl[:certificate] if ssl[:certificate]
opts[:private_key] = ssl[:private_key] if ssl[:private_key]
# https://github.com/geemus/excon/issues/106
# https://github.com/jruby/jruby-ossl/issues/19
opts[:nonblock] = false
end
if ( req = env[:request] )
if req[:timeout]
opts[:read_timeout] = req[:timeout]
opts[:connect_timeout] = req[:timeout]
opts[:write_timeout] = req[:timeout]
end
if req[:open_timeout]
opts[:connect_timeout] = req[:open_timeout]
opts[:write_timeout] = req[:open_timeout]
end
if req[:proxy]
opts[:proxy] = {
:host => req[:proxy][:uri].host,
:hostname => req[:proxy][:uri].hostname,
:port => req[:proxy][:uri].port,
:scheme => req[:proxy][:uri].scheme,
:user => req[:proxy][:user],
:password => req[:proxy][:password]
}
end
end
conn = ::Excon.new(env[:url].to_s, opts.merge(@connection_options))
resp = conn.request \
:method => env[:method].to_s.upcase,
:headers => env[:request_headers],
:body => read_body(env)
save_response(env, resp.status.to_i, resp.body, resp.headers)
@app.call env
rescue ::Excon::Errors::SocketError => err
if err.message =~ /\btimeout\b/
raise Error::TimeoutError, err
elsif err.message =~ /\bcertificate\b/
raise Faraday::SSLError, err
else
raise Error::ConnectionFailed, err
end
rescue ::Excon::Errors::Timeout => err
raise Error::TimeoutError, err
end
# TODO: support streaming requests
def read_body(env)
env[:body].respond_to?(:read) ? env[:body].read : env[:body]
end
end
end
end

@ -0,0 +1,117 @@
module Faraday
class Adapter
class HTTPClient < Faraday::Adapter
dependency 'httpclient'
def client
@client ||= ::HTTPClient.new
end
def call(env)
super
# enable compression
client.transparent_gzip_decompression = true
if req = env[:request]
if proxy = req[:proxy]
configure_proxy proxy
end
if bind = req[:bind]
configure_socket bind
end
configure_timeouts req
end
if env[:url].scheme == 'https' && ssl = env[:ssl]
configure_ssl ssl
end
# TODO Don't stream yet.
# https://github.com/nahi/httpclient/pull/90
env[:body] = env[:body].read if env[:body].respond_to? :read
resp = client.request env[:method], env[:url],
:body => env[:body],
:header => env[:request_headers]
save_response env, resp.status, resp.body, resp.headers
@app.call env
rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT
raise Faraday::Error::TimeoutError, $!
rescue ::HTTPClient::BadResponseError => err
if err.message.include?('status 407')
raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
else
raise Faraday::Error::ClientError, $!
end
rescue Errno::ECONNREFUSED, EOFError
raise Faraday::Error::ConnectionFailed, $!
rescue => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise Faraday::SSLError, err
else
raise
end
end
def configure_socket(bind)
client.socket_local.host = bind[:host]
client.socket_local.port = bind[:port]
end
def configure_proxy(proxy)
client.proxy = proxy[:uri]
if proxy[:user] && proxy[:password]
client.set_proxy_auth proxy[:user], proxy[:password]
end
end
def configure_ssl(ssl)
ssl_config = client.ssl_config
ssl_config.verify_mode = ssl_verify_mode(ssl)
ssl_config.cert_store = ssl_cert_store(ssl)
ssl_config.add_trust_ca ssl[:ca_file] if ssl[:ca_file]
ssl_config.add_trust_ca ssl[:ca_path] if ssl[:ca_path]
ssl_config.client_cert = ssl[:client_cert] if ssl[:client_cert]
ssl_config.client_key = ssl[:client_key] if ssl[:client_key]
ssl_config.verify_depth = ssl[:verify_depth] if ssl[:verify_depth]
end
def configure_timeouts(req)
if req[:timeout]
client.connect_timeout = req[:timeout]
client.receive_timeout = req[:timeout]
client.send_timeout = req[:timeout]
end
if req[:open_timeout]
client.connect_timeout = req[:open_timeout]
client.send_timeout = req[:open_timeout]
end
end
def ssl_cert_store(ssl)
return ssl[:cert_store] if ssl[:cert_store]
# Use the default cert store by default, i.e. system ca certs
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
cert_store
end
def ssl_verify_mode(ssl)
ssl[:verify_mode] || begin
if ssl.fetch(:verify, true)
OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
else
OpenSSL::SSL::VERIFY_NONE
end
end
end
end
end
end

@ -0,0 +1,130 @@
begin
require 'net/https'
rescue LoadError
warn "Warning: no such file to load -- net/https. Make sure openssl is installed if you want ssl support"
require 'net/http'
end
require 'zlib'
module Faraday
class Adapter
class NetHttp < Faraday::Adapter
NET_HTTP_EXCEPTIONS = [
EOFError,
Errno::ECONNABORTED,
Errno::ECONNREFUSED,
Errno::ECONNRESET,
Errno::EHOSTUNREACH,
Errno::EINVAL,
Errno::ENETUNREACH,
Net::HTTPBadResponse,
Net::HTTPHeaderSyntaxError,
Net::ProtocolError,
SocketError,
Zlib::GzipFile::Error,
]
NET_HTTP_EXCEPTIONS << OpenSSL::SSL::SSLError if defined?(OpenSSL)
NET_HTTP_EXCEPTIONS << Net::OpenTimeout if defined?(Net::OpenTimeout)
def call(env)
super
with_net_http_connection(env) do |http|
configure_ssl(http, env[:ssl]) if env[:url].scheme == 'https' and env[:ssl]
req = env[:request]
http.read_timeout = http.open_timeout = req[:timeout] if req[:timeout]
http.open_timeout = req[:open_timeout] if req[:open_timeout]
begin
http_response = perform_request(http, env)
rescue *NET_HTTP_EXCEPTIONS => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise Faraday::SSLError, err
else
raise Error::ConnectionFailed, err
end
end
save_response(env, http_response.code.to_i, http_response.body || '') do |response_headers|
http_response.each_header do |key, value|
response_headers[key] = value
end
end
end
@app.call env
rescue Timeout::Error, Errno::ETIMEDOUT => err
raise Faraday::Error::TimeoutError, err
end
def create_request(env)
request = Net::HTTPGenericRequest.new \
env[:method].to_s.upcase, # request method
!!env[:body], # is there request body
:head != env[:method], # is there response body
env[:url].request_uri, # request uri path
env[:request_headers] # request headers
if env[:body].respond_to?(:read)
request.body_stream = env[:body]
else
request.body = env[:body]
end
request
end
def perform_request(http, env)
if :get == env[:method] and !env[:body]
# prefer `get` to `request` because the former handles gzip (ruby 1.9)
http.get env[:url].request_uri, env[:request_headers]
else
http.request create_request(env)
end
end
def with_net_http_connection(env)
yield net_http_connection(env)
end
def net_http_connection(env)
if proxy = env[:request][:proxy]
Net::HTTP::Proxy(proxy[:uri].host, proxy[:uri].port, proxy[:user], proxy[:password])
else
Net::HTTP
end.new(env[:url].host, env[:url].port || (env[:url].scheme == 'https' ? 443 : 80))
end
def configure_ssl(http, ssl)
http.use_ssl = true
http.verify_mode = ssl_verify_mode(ssl)
http.cert_store = ssl_cert_store(ssl)
http.cert = ssl[:client_cert] if ssl[:client_cert]
http.key = ssl[:client_key] if ssl[:client_key]
http.ca_file = ssl[:ca_file] if ssl[:ca_file]
http.ca_path = ssl[:ca_path] if ssl[:ca_path]
http.verify_depth = ssl[:verify_depth] if ssl[:verify_depth]
http.ssl_version = ssl[:version] if ssl[:version]
end
def ssl_cert_store(ssl)
return ssl[:cert_store] if ssl[:cert_store]
# Use the default cert store by default, i.e. system ca certs
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
cert_store
end
def ssl_verify_mode(ssl)
ssl[:verify_mode] || begin
if ssl.fetch(:verify, true)
OpenSSL::SSL::VERIFY_PEER
else
OpenSSL::SSL::VERIFY_NONE
end
end
end
end
end
end

@ -0,0 +1,49 @@
# Rely on autoloading instead of explicit require; helps avoid the "already
# initialized constant" warning on Ruby 1.8.7 when NetHttp is refereced below.
# require 'faraday/adapter/net_http'
module Faraday
class Adapter
class NetHttpPersistent < NetHttp
dependency 'net/http/persistent'
def with_net_http_connection(env)
if proxy = env[:request][:proxy]
proxy_uri = ::URI::HTTP === proxy[:uri] ? proxy[:uri].dup : ::URI.parse(proxy[:uri].to_s)
proxy_uri.user = proxy_uri.password = nil
# awful patch for net-http-persistent 2.8 not unescaping user/password
(class << proxy_uri; self; end).class_eval do
define_method(:user) { proxy[:user] }
define_method(:password) { proxy[:password] }
end if proxy[:user]
end
yield Net::HTTP::Persistent.new 'Faraday', proxy_uri
end
def perform_request(http, env)
http.request env[:url], create_request(env)
rescue Errno::ETIMEDOUT => error
raise Faraday::Error::TimeoutError, error
rescue Net::HTTP::Persistent::Error => error
if error.message.include? 'Timeout'
raise Faraday::Error::TimeoutError, error
elsif error.message.include? 'connection refused'
raise Faraday::Error::ConnectionFailed, error
else
raise
end
end
def configure_ssl(http, ssl)
http.verify_mode = ssl_verify_mode(ssl)
http.cert_store = ssl_cert_store(ssl)
http.certificate = ssl[:client_cert] if ssl[:client_cert]
http.private_key = ssl[:client_key] if ssl[:client_key]
http.ca_file = ssl[:ca_file] if ssl[:ca_file]
http.ssl_version = ssl[:version] if ssl[:version]
end
end
end
end

@ -0,0 +1,78 @@
module Faraday
class Adapter
class Patron < Faraday::Adapter
dependency 'patron'
def initialize(app, &block)
super(app)
@block = block
end
def call(env)
super
# TODO: support streaming requests
env[:body] = env[:body].read if env[:body].respond_to? :read
session = @session ||= create_session
if req = env[:request]
session.timeout = session.connect_timeout = req[:timeout] if req[:timeout]
session.connect_timeout = req[:open_timeout] if req[:open_timeout]
if proxy = req[:proxy]
proxy_uri = proxy[:uri].dup
proxy_uri.user = proxy[:user] && Utils.escape(proxy[:user]).gsub('+', '%20')
proxy_uri.password = proxy[:password] && Utils.escape(proxy[:password]).gsub('+', '%20')
session.proxy = proxy_uri.to_s
end
end
response = begin
data = env[:body] ? env[:body].to_s : nil
session.request(env[:method], env[:url].to_s, env[:request_headers], :data => data)
rescue Errno::ECONNREFUSED, ::Patron::ConnectionFailed
raise Error::ConnectionFailed, $!
end
save_response(env, response.status, response.body, response.headers)
@app.call env
rescue ::Patron::TimeoutError => err
if err.message == "Connection time-out"
raise Faraday::Error::ConnectionFailed, err
else
raise Faraday::Error::TimeoutError, err
end
rescue ::Patron::Error => err
if err.message.include?("code 407")
raise Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
else
raise Error::ConnectionFailed, err
end
end
if loaded? && defined?(::Patron::Request::VALID_ACTIONS)
# HAX: helps but doesn't work completely
# https://github.com/toland/patron/issues/34
::Patron::Request::VALID_ACTIONS.tap do |actions|
if actions[0].is_a?(Symbol)
actions << :patch unless actions.include? :patch
actions << :options unless actions.include? :options
else
# Patron 0.4.20 and up
actions << "PATCH" unless actions.include? "PATCH"
actions << "OPTIONS" unless actions.include? "OPTIONS"
end
end
end
def create_session
session = ::Patron::Session.new
session.insecure = true
@block.call(session) if @block
session
end
end
end
end

@ -0,0 +1,58 @@
module Faraday
class Adapter
# Sends requests to a Rack app.
#
# Examples
#
# class MyRackApp
# def call(env)
# [200, {'Content-Type' => 'text/html'}, ["hello world"]]
# end
# end
#
# Faraday.new do |conn|
# conn.adapter :rack, MyRackApp.new
# end
class Rack < Faraday::Adapter
dependency 'rack/test'
# not prefixed with "HTTP_"
SPECIAL_HEADERS = %w[ CONTENT_LENGTH CONTENT_TYPE ]
def initialize(faraday_app, rack_app)
super(faraday_app)
mock_session = ::Rack::MockSession.new(rack_app)
@session = ::Rack::Test::Session.new(mock_session)
end
def call(env)
super
rack_env = {
:method => env[:method],
:input => env[:body].respond_to?(:read) ? env[:body].read : env[:body],
'rack.url_scheme' => env[:url].scheme
}
env[:request_headers].each do |name, value|
name = name.upcase.tr('-', '_')
name = "HTTP_#{name}" unless SPECIAL_HEADERS.include? name
rack_env[name] = value
end if env[:request_headers]
timeout = env[:request][:timeout] || env[:request][:open_timeout]
response = if timeout
Timer.timeout(timeout, Faraday::Error::TimeoutError) { execute_request(env, rack_env) }
else
execute_request(env, rack_env)
end
save_response(env, response.status, response.body, response.headers)
@app.call env
end
def execute_request(env, rack_env)
@session.request(env[:url].to_s, rack_env)
end
end
end
end

@ -0,0 +1,162 @@
module Faraday
class Adapter
# test = Faraday::Connection.new do
# use Faraday::Adapter::Test do |stub|
# stub.get '/nigiri/sake.json' do
# [200, {}, 'hi world']
# end
# end
# end
#
# resp = test.get '/nigiri/sake.json'
# resp.body # => 'hi world'
#
class Test < Faraday::Adapter
attr_accessor :stubs
class Stubs
class NotFound < StandardError
end
def initialize
# {:get => [Stub, Stub]}
@stack, @consumed = {}, {}
yield(self) if block_given?
end
def empty?
@stack.empty?
end
def match(request_method, path, headers, body)
return false if !@stack.key?(request_method)
stack = @stack[request_method]
consumed = (@consumed[request_method] ||= [])
if stub = matches?(stack, path, headers, body)
consumed << stack.delete(stub)
stub
else
matches?(consumed, path, headers, body)
end
end
def get(path, headers = {}, &block)
new_stub(:get, path, headers, &block)
end
def head(path, headers = {}, &block)
new_stub(:head, path, headers, &block)
end
def post(path, body=nil, headers = {}, &block)
new_stub(:post, path, headers, body, &block)
end
def put(path, body=nil, headers = {}, &block)
new_stub(:put, path, headers, body, &block)
end
def patch(path, body=nil, headers = {}, &block)
new_stub(:patch, path, headers, body, &block)
end
def delete(path, headers = {}, &block)
new_stub(:delete, path, headers, &block)
end
def options(path, headers = {}, &block)
new_stub(:options, path, headers, &block)
end
# Raises an error if any of the stubbed calls have not been made.
def verify_stubbed_calls
failed_stubs = []
@stack.each do |method, stubs|
unless stubs.size == 0
failed_stubs.concat(stubs.map {|stub|
"Expected #{method} #{stub}."
})
end
end
raise failed_stubs.join(" ") unless failed_stubs.size == 0
end
protected
def new_stub(request_method, path, headers = {}, body=nil, &block)
normalized_path = Faraday::Utils.normalize_path(path)
(@stack[request_method] ||= []) << Stub.new(normalized_path, headers, body, block)
end
def matches?(stack, path, headers, body)
stack.detect { |stub| stub.matches?(path, headers, body) }
end
end
class Stub < Struct.new(:path, :params, :headers, :body, :block)
def initialize(full, headers, body, block)
path, query = full.split('?')
params = query ?
Faraday::Utils.parse_nested_query(query) :
{}
super(path, params, headers, body, block)
end
def matches?(request_uri, request_headers, request_body)
request_path, request_query = request_uri.split('?')
request_params = request_query ?
Faraday::Utils.parse_nested_query(request_query) :
{}
request_path == path &&
params_match?(request_params) &&
(body.to_s.size.zero? || request_body == body) &&
headers_match?(request_headers)
end
def params_match?(request_params)
params.keys.all? do |key|
request_params[key] == params[key]
end
end
def headers_match?(request_headers)
headers.keys.all? do |key|
request_headers[key] == headers[key]
end
end
def to_s
"#{path} #{body}"
end
end
def initialize(app, stubs=nil, &block)
super(app)
@stubs = stubs || Stubs.new
configure(&block) if block
end
def configure
yield(stubs)
end
def call(env)
super
normalized_path = Faraday::Utils.normalize_path(env[:url])
params_encoder = env.request.params_encoder || Faraday::Utils.default_params_encoder
if stub = stubs.match(env[:method], normalized_path, env.request_headers, env[:body])
env[:params] = (query = env[:url].query) ?
params_encoder.decode(query) :
{}
status, headers, body = stub.block.call(env)
save_response(env, status, body, headers)
else
raise Stubs::NotFound, "no stubbed request for #{env[:method]} #{normalized_path} #{env[:body]}"
end
@app.call(env)
end
end
end
end

@ -0,0 +1,123 @@
module Faraday
class Adapter
class Typhoeus < Faraday::Adapter
self.supports_parallel = true
def self.setup_parallel_manager(options = {})
options.empty? ? ::Typhoeus::Hydra.hydra : ::Typhoeus::Hydra.new(options)
end
dependency 'typhoeus'
def call(env)
super
perform_request env
@app.call env
end
def perform_request(env)
read_body env
hydra = env[:parallel_manager] || self.class.setup_parallel_manager
hydra.queue request(env)
hydra.run unless parallel?(env)
rescue Errno::ECONNREFUSED
raise Error::ConnectionFailed, $!
end
# TODO: support streaming requests
def read_body(env)
env[:body] = env[:body].read if env[:body].respond_to? :read
end
def request(env)
method = env[:method]
# For some reason, prevents Typhoeus from using "100-continue".
# We want this because Webrick 1.3.1 can't seem to handle it w/ PUT.
method = method.to_s.upcase if method == :put
req = ::Typhoeus::Request.new env[:url].to_s,
:method => method,
:body => env[:body],
:headers => env[:request_headers],
:disable_ssl_peer_verification => (env[:ssl] && env[:ssl].disable?)
configure_ssl req, env
configure_proxy req, env
configure_timeout req, env
configure_socket req, env
req.on_complete do |resp|
if resp.timed_out?
if parallel?(env)
# TODO: error callback in async mode
else
raise Faraday::Error::TimeoutError, "request timed out"
end
end
case resp.curl_return_code
when 0
# everything OK
when 7
raise Error::ConnectionFailed, resp.curl_error_message
when 60
raise Faraday::SSLError, resp.curl_error_message
else
raise Error::ClientError, resp.curl_error_message
end
save_response(env, resp.code, resp.body) do |response_headers|
response_headers.parse resp.headers
end
# in async mode, :response is initialized at this point
env[:response].finish(env) if parallel?(env)
end
req
end
def configure_ssl(req, env)
ssl = env[:ssl]
req.ssl_version = ssl[:version] if ssl[:version]
req.ssl_cert = ssl[:client_cert] if ssl[:client_cert]
req.ssl_key = ssl[:client_key] if ssl[:client_key]
req.ssl_cacert = ssl[:ca_file] if ssl[:ca_file]
req.ssl_capath = ssl[:ca_path] if ssl[:ca_path]
end
def configure_proxy(req, env)
proxy = request_options(env)[:proxy]
return unless proxy
req.proxy = "#{proxy[:uri].host}:#{proxy[:uri].port}"
if proxy[:user] && proxy[:password]
req.proxy_username = proxy[:user]
req.proxy_password = proxy[:password]
end
end
def configure_timeout(req, env)
env_req = request_options(env)
req.timeout = req.connect_timeout = (env_req[:timeout] * 1000) if env_req[:timeout]
req.connect_timeout = (env_req[:open_timeout] * 1000) if env_req[:open_timeout]
end
def configure_socket(req, env)
if bind = request_options(env)[:bind]
req.interface = bind[:host]
end
end
def request_options(env)
env[:request]
end
def parallel?(env)
!!env[:parallel_manager]
end
end
end
end

@ -0,0 +1,84 @@
module Faraday
# Internal: Adds the ability for other modules to manage autoloadable
# constants.
module AutoloadHelper
# Internal: Registers the constants to be auto loaded.
#
# prefix - The String require prefix. If the path is inside Faraday, then
# it will be prefixed with the root path of this loaded Faraday
# version.
# options - Hash of Symbol => String library names.
#
# Examples.
#
# Faraday.autoload_all 'faraday/foo',
# :Bar => 'bar'
#
# # requires faraday/foo/bar to load Faraday::Bar.
# Faraday::Bar
#
#
# Returns nothing.
def autoload_all(prefix, options)
if prefix =~ /^faraday(\/|$)/i
prefix = File.join(Faraday.root_path, prefix)
end
options.each do |const_name, path|
autoload const_name, File.join(prefix, path)
end
end
# Internal: Loads each autoloaded constant. If thread safety is a concern,
# wrap this in a Mutex.
#
# Returns nothing.
def load_autoloaded_constants
constants.each do |const|
const_get(const) if autoload?(const)
end
end
# Internal: Filters the module's contents with those that have been already
# autoloaded.
#
# Returns an Array of Class/Module objects.
def all_loaded_constants
constants.map { |c| const_get(c) }.
select { |a| a.respond_to?(:loaded?) && a.loaded? }
end
end
class Adapter
extend AutoloadHelper
autoload_all 'faraday/adapter',
:NetHttp => 'net_http',
:NetHttpPersistent => 'net_http_persistent',
:Typhoeus => 'typhoeus',
:EMSynchrony => 'em_synchrony',
:EMHttp => 'em_http',
:Patron => 'patron',
:Excon => 'excon',
:Test => 'test',
:Rack => 'rack',
:HTTPClient => 'httpclient'
end
class Request
extend AutoloadHelper
autoload_all 'faraday/request',
:UrlEncoded => 'url_encoded',
:Multipart => 'multipart',
:Retry => 'retry',
:Authorization => 'authorization',
:BasicAuthentication => 'basic_authentication',
:TokenAuthentication => 'token_authentication',
:Instrumentation => 'instrumentation'
end
class Response
extend AutoloadHelper
autoload_all 'faraday/response',
:RaiseError => 'raise_error',
:Logger => 'logger'
end
end

@ -0,0 +1,437 @@
module Faraday
# Public: Connection objects manage the default properties and the middleware
# stack for fulfilling an HTTP request.
#
# Examples
#
# conn = Faraday::Connection.new 'http://sushi.com'
#
# # GET http://sushi.com/nigiri
# conn.get 'nigiri'
# # => #<Faraday::Response>
#
class Connection
# A Set of allowed HTTP verbs.
METHODS = Set.new [:get, :post, :put, :delete, :head, :patch, :options]
# Public: Returns a Hash of URI query unencoded key/value pairs.
attr_reader :params
# Public: Returns a Hash of unencoded HTTP header key/value pairs.
attr_reader :headers
# Public: Returns a URI with the prefix used for all requests from this
# Connection. This includes a default host name, scheme, port, and path.
attr_reader :url_prefix
# Public: Returns the Faraday::Builder for this Connection.
attr_reader :builder
# Public: Returns a Hash of the request options.
attr_reader :options
# Public: Returns a Hash of the SSL options.
attr_reader :ssl
# Public: Returns the parallel manager for this Connection.
attr_reader :parallel_manager
# Public: Sets the default parallel manager for this connection.
attr_writer :default_parallel_manager
# Public: Initializes a new Faraday::Connection.
#
# url - URI or String base URL to use as a prefix for all
# requests (optional).
# options - Hash or Faraday::ConnectionOptions.
# :url - URI or String base URL (default: "http:/").
# :params - Hash of URI query unencoded key/value pairs.
# :headers - Hash of unencoded HTTP header key/value pairs.
# :request - Hash of request options.
# :ssl - Hash of SSL options.
# :proxy - URI, String or Hash of HTTP proxy options
# (default: "http_proxy" environment variable).
# :uri - URI or String
# :user - String (optional)
# :password - String (optional)
def initialize(url = nil, options = nil)
if url.is_a?(Hash)
options = ConnectionOptions.from(url)
url = options.url
else
options = ConnectionOptions.from(options)
end
@parallel_manager = nil
@headers = Utils::Headers.new
@params = Utils::ParamsHash.new
@options = options.request
@ssl = options.ssl
@default_parallel_manager = options.parallel_manager
@builder = options.builder || begin
# pass an empty block to Builder so it doesn't assume default middleware
options.new_builder(block_given? ? Proc.new { |b| } : nil)
end
self.url_prefix = url || 'http:/'
@params.update(options.params) if options.params
@headers.update(options.headers) if options.headers
@proxy = nil
proxy(options.fetch(:proxy) {
uri = ENV['http_proxy']
if uri && !uri.empty?
uri = 'http://' + uri if uri !~ /^http/i
uri
end
})
yield(self) if block_given?
@headers[:user_agent] ||= "Faraday v#{VERSION}"
end
# Public: Sets the Hash of URI query unencoded key/value pairs.
def params=(hash)
@params.replace hash
end
# Public: Sets the Hash of unencoded HTTP header key/value pairs.
def headers=(hash)
@headers.replace hash
end
extend Forwardable
def_delegators :builder, :build, :use, :request, :response, :adapter, :app
# Public: Makes an HTTP request without a body.
#
# url - The optional String base URL to use as a prefix for all
# requests. Can also be the options Hash.
# params - Hash of URI query unencoded key/value pairs.
# headers - Hash of unencoded HTTP header key/value pairs.
#
# Examples
#
# conn.get '/items', {:page => 1}, :accept => 'application/json'
# conn.head '/items/1'
#
# # ElasticSearch example sending a body with GET.
# conn.get '/twitter/tweet/_search' do |req|
# req.headers[:content_type] = 'application/json'
# req.params[:routing] = 'kimchy'
# req.body = JSON.generate(:query => {...})
# end
#
# Yields a Faraday::Response for further request customizations.
# Returns a Faraday::Response.
#
# Signature
#
# <verb>(url = nil, params = nil, headers = nil)
#
# verb - An HTTP verb: get, head, or delete.
%w[get head delete].each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(url = nil, params = nil, headers = nil)
run_request(:#{method}, url, nil, headers) { |request|
request.params.update(params) if params
yield(request) if block_given?
}
end
RUBY
end
# Public: Makes an HTTP request with a body.
#
# url - The optional String base URL to use as a prefix for all
# requests. Can also be the options Hash.
# body - The String body for the request.
# headers - Hash of unencoded HTTP header key/value pairs.
#
# Examples
#
# conn.post '/items', data, :content_type => 'application/json'
#
# # Simple ElasticSearch indexing sample.
# conn.post '/twitter/tweet' do |req|
# req.headers[:content_type] = 'application/json'
# req.params[:routing] = 'kimchy'
# req.body = JSON.generate(:user => 'kimchy', ...)
# end
#
# Yields a Faraday::Response for further request customizations.
# Returns a Faraday::Response.
#
# Signature
#
# <verb>(url = nil, body = nil, headers = nil)
#
# verb - An HTTP verb: post, put, or patch.
%w[post put patch].each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(url = nil, body = nil, headers = nil, &block)
run_request(:#{method}, url, body, headers, &block)
end
RUBY
end
# Public: Sets up the Authorization header with these credentials, encoded
# with base64.
#
# login - The authentication login.
# pass - The authentication password.
#
# Examples
#
# conn.basic_auth 'Aladdin', 'open sesame'
# conn.headers['Authorization']
# # => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
#
# Returns nothing.
def basic_auth(login, pass)
set_authorization_header(:basic_auth, login, pass)
end
# Public: Sets up the Authorization header with the given token.
#
# token - The String token.
# options - Optional Hash of extra token options.
#
# Examples
#
# conn.token_auth 'abcdef', :foo => 'bar'
# conn.headers['Authorization']
# # => "Token token=\"abcdef\",
# foo=\"bar\""
#
# Returns nothing.
def token_auth(token, options = nil)
set_authorization_header(:token_auth, token, options)
end
# Public: Sets up a custom Authorization header.
#
# type - The String authorization type.
# token - The String or Hash token. A String value is taken literally, and
# a Hash is encoded into comma separated key/value pairs.
#
# Examples
#
# conn.authorization :Bearer, 'mF_9.B5f-4.1JqM'
# conn.headers['Authorization']
# # => "Bearer mF_9.B5f-4.1JqM"
#
# conn.authorization :Token, :token => 'abcdef', :foo => 'bar'
# conn.headers['Authorization']
# # => "Token token=\"abcdef\",
# foo=\"bar\""
#
# Returns nothing.
def authorization(type, token)
set_authorization_header(:authorization, type, token)
end
# Internal: Traverse the middleware stack in search of a
# parallel-capable adapter.
#
# Yields in case of not found.
#
# Returns a parallel manager or nil if not found.
def default_parallel_manager
@default_parallel_manager ||= begin
handler = @builder.handlers.detect do |h|
h.klass.respond_to?(:supports_parallel?) and h.klass.supports_parallel?
end
if handler
handler.klass.setup_parallel_manager
elsif block_given?
yield
end
end
end
# Public: Determine if this Faraday::Connection can make parallel requests.
#
# Returns true or false.
def in_parallel?
!!@parallel_manager
end
# Public: Sets up the parallel manager to make a set of requests.
#
# manager - The parallel manager that this Connection's Adapter uses.
#
# Yields a block to execute multiple requests.
# Returns nothing.
def in_parallel(manager = nil)
@parallel_manager = manager || default_parallel_manager {
warn "Warning: `in_parallel` called but no parallel-capable adapter on Faraday stack"
warn caller[2,10].join("\n")
nil
}
yield
@parallel_manager && @parallel_manager.run
ensure
@parallel_manager = nil
end
# Public: Gets or Sets the Hash proxy options.
def proxy(arg = nil)
return @proxy if arg.nil?
@proxy = ProxyOptions.from(arg)
end
def_delegators :url_prefix, :scheme, :scheme=, :host, :host=, :port, :port=
def_delegator :url_prefix, :path, :path_prefix
# Public: Parses the giving url with URI and stores the individual
# components in this connection. These components serve as defaults for
# requests made by this connection.
#
# url - A String or URI.
#
# Examples
#
# conn = Faraday::Connection.new { ... }
# conn.url_prefix = "https://sushi.com/api"
# conn.scheme # => https
# conn.path_prefix # => "/api"
#
# conn.get("nigiri?page=2") # accesses https://sushi.com/api/nigiri
#
# Returns the parsed URI from teh given input..
def url_prefix=(url, encoder = nil)
uri = @url_prefix = Utils.URI(url)
self.path_prefix = uri.path
params.merge_query(uri.query, encoder)
uri.query = nil
with_uri_credentials(uri) do |user, password|
basic_auth user, password
uri.user = uri.password = nil
end
uri
end
# Public: Sets the path prefix and ensures that it always has a leading
# slash.
#
# value - A String.
#
# Returns the new String path prefix.
def path_prefix=(value)
url_prefix.path = if value
value = '/' + value unless value[0,1] == '/'
value
end
end
# Public: Takes a relative url for a request and combines it with the defaults
# set on the connection instance.
#
# conn = Faraday::Connection.new { ... }
# conn.url_prefix = "https://sushi.com/api?token=abc"
# conn.scheme # => https
# conn.path_prefix # => "/api"
#
# conn.build_url("nigiri?page=2") # => https://sushi.com/api/nigiri?token=abc&page=2
# conn.build_url("nigiri", :page => 2) # => https://sushi.com/api/nigiri?token=abc&page=2
#
def build_url(url = nil, extra_params = nil)
uri = build_exclusive_url(url)
query_values = params.dup.merge_query(uri.query, options.params_encoder)
query_values.update extra_params if extra_params
uri.query = query_values.empty? ? nil : query_values.to_query(options.params_encoder)
uri
end
# Builds and runs the Faraday::Request.
#
# method - The Symbol HTTP method.
# url - The String or URI to access.
# body - The String body
# headers - Hash of unencoded HTTP header key/value pairs.
#
# Returns a Faraday::Response.
def run_request(method, url, body, headers)
if !METHODS.include?(method)
raise ArgumentError, "unknown http method: #{method}"
end
request = build_request(method) do |req|
req.url(url) if url
req.headers.update(headers) if headers
req.body = body if body
yield(req) if block_given?
end
builder.build_response(self, request)
end
# Creates and configures the request object.
#
# Returns the new Request.
def build_request(method)
Request.create(method) do |req|
req.params = self.params.dup
req.headers = self.headers.dup
req.options = self.options.merge(:proxy => self.proxy)
yield(req) if block_given?
end
end
# Internal: Build an absolute URL based on url_prefix.
#
# url - A String or URI-like object
# params - A Faraday::Utils::ParamsHash to replace the query values
# of the resulting url (default: nil).
#
# Returns the resulting URI instance.
def build_exclusive_url(url = nil, params = nil, params_encoder = nil)
url = nil if url.respond_to?(:empty?) and url.empty?
base = url_prefix
if url and base.path and base.path !~ /\/$/
base = base.dup
base.path = base.path + '/' # ensure trailing slash
end
uri = url ? base + url : base
uri.query = params.to_query(params_encoder || options.params_encoder) if params
uri.query = nil if uri.query and uri.query.empty?
uri
end
# Internal: Creates a duplicate of this Faraday::Connection.
#
# Returns a Faraday::Connection.
def dup
self.class.new(build_exclusive_url,
:headers => headers.dup,
:params => params.dup,
:builder => builder.dup,
:ssl => ssl.dup,
:request => options.dup)
end
# Internal: Yields username and password extracted from a URI if they both exist.
def with_uri_credentials(uri)
if uri.user and uri.password
yield(Utils.unescape(uri.user), Utils.unescape(uri.password))
end
end
def set_authorization_header(header_type, *args)
header = Faraday::Request.lookup_middleware(header_type).
header(*args)
headers[Faraday::Request::Authorization::KEY] = header
end
end
end

@ -0,0 +1,53 @@
module Faraday
class Error < StandardError; end
class MissingDependency < Error; end
class ClientError < Error
attr_reader :response
def initialize(ex, response = nil)
@wrapped_exception = nil
@response = response
if ex.respond_to?(:backtrace)
super(ex.message)
@wrapped_exception = ex
elsif ex.respond_to?(:each_key)
super("the server responded with status #{ex[:status]}")
@response = ex
else
super(ex.to_s)
end
end
def backtrace
if @wrapped_exception
@wrapped_exception.backtrace
else
super
end
end
def inspect
%(#<#{self.class}>)
end
end
class ConnectionFailed < ClientError; end
class ResourceNotFound < ClientError; end
class ParsingError < ClientError; end
class TimeoutError < ClientError
def initialize(ex = nil)
super(ex || "timeout")
end
end
class SSLError < ClientError
end
[:MissingDependency, :ClientError, :ConnectionFailed, :ResourceNotFound,
:ParsingError, :TimeoutError, :SSLError].each do |const|
Error.const_set(const, Faraday.const_get(const))
end
end

@ -0,0 +1,37 @@
module Faraday
class Middleware
extend MiddlewareRegistry
class << self
attr_accessor :load_error
private :load_error=
end
self.load_error = nil
# Executes a block which should try to require and reference dependent libraries
def self.dependency(lib = nil)
lib ? require(lib) : yield
rescue LoadError, NameError => error
self.load_error = error
end
def self.new(*)
raise "missing dependency for #{self}: #{load_error.message}" unless loaded?
super
end
def self.loaded?
load_error.nil?
end
def self.inherited(subclass)
super
subclass.send(:load_error=, self.load_error)
end
def initialize(app = nil)
@app = app
end
end
end

@ -0,0 +1,359 @@
module Faraday
# Subclasses Struct with some special helpers for converting from a Hash to
# a Struct.
class Options < Struct
# Public
def self.from(value)
value ? new.update(value) : new
end
# Public
def each
return to_enum(:each) unless block_given?
members.each do |key|
yield(key.to_sym, send(key))
end
end
# Public
def update(obj)
obj.each do |key, value|
if sub_options = self.class.options_for(key)
value = sub_options.from(value) if value
elsif Hash === value
hash = {}
value.each do |hash_key, hash_value|
hash[hash_key] = hash_value
end
value = hash
end
self.send("#{key}=", value) unless value.nil?
end
self
end
alias merge! update
# Public
def delete(key)
value = send(key)
send("#{key}=", nil)
value
end
# Public
def clear
members.each { |member| delete(member) }
end
# Public
def merge(value)
dup.update(value)
end
# Public
def fetch(key, *args)
unless symbolized_key_set.include?(key.to_sym)
key_setter = "#{key}="
if args.size > 0
send(key_setter, args.first)
elsif block_given?
send(key_setter, Proc.new.call(key))
else
raise self.class.fetch_error_class, "key not found: #{key.inspect}"
end
end
send(key)
end
# Public
def values_at(*keys)
keys.map { |key| send(key) }
end
# Public
def keys
members.reject { |member| send(member).nil? }
end
# Public
def empty?
keys.empty?
end
# Public
def each_key
return to_enum(:each_key) unless block_given?
keys.each do |key|
yield(key)
end
end
# Public
def key?(key)
keys.include?(key)
end
alias has_key? key?
# Public
def each_value
return to_enum(:each_value) unless block_given?
values.each do |value|
yield(value)
end
end
# Public
def value?(value)
values.include?(value)
end
alias has_value? value?
# Public
def to_hash
hash = {}
members.each do |key|
value = send(key)
hash[key.to_sym] = value unless value.nil?
end
hash
end
# Internal
def inspect
values = []
members.each do |member|
value = send(member)
values << "#{member}=#{value.inspect}" if value
end
values = values.empty? ? ' (empty)' : (' ' << values.join(", "))
%(#<#{self.class}#{values}>)
end
# Internal
def self.options(mapping)
attribute_options.update(mapping)
end
# Internal
def self.options_for(key)
attribute_options[key]
end
# Internal
def self.attribute_options
@attribute_options ||= {}
end
def self.memoized(key)
memoized_attributes[key.to_sym] = Proc.new
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{key}() self[:#{key}]; end
RUBY
end
def self.memoized_attributes
@memoized_attributes ||= {}
end
def [](key)
key = key.to_sym
if method = self.class.memoized_attributes[key]
super(key) || (self[key] = instance_eval(&method))
else
super
end
end
def symbolized_key_set
@symbolized_key_set ||= Set.new(keys.map { |k| k.to_sym })
end
def self.inherited(subclass)
super
subclass.attribute_options.update(attribute_options)
subclass.memoized_attributes.update(memoized_attributes)
end
def self.fetch_error_class
@fetch_error_class ||= if Object.const_defined?(:KeyError)
::KeyError
else
::IndexError
end
end
end
class RequestOptions < Options.new(:params_encoder, :proxy, :bind,
:timeout, :open_timeout, :boundary,
:oauth)
def []=(key, value)
if key && key.to_sym == :proxy
super(key, value ? ProxyOptions.from(value) : nil)
else
super(key, value)
end
end
end
class SSLOptions < Options.new(:verify, :ca_file, :ca_path, :verify_mode,
:cert_store, :client_cert, :client_key, :certificate, :private_key, :verify_depth, :version)
def verify?
verify != false
end
def disable?
!verify?
end
end
class ProxyOptions < Options.new(:uri, :user, :password)
extend Forwardable
def_delegators :uri, :scheme, :scheme=, :host, :host=, :port, :port=, :path, :path=
def self.from(value)
case value
when String
value = {:uri => Utils.URI(value)}
when URI
value = {:uri => value}
when Hash, Options
if uri = value.delete(:uri)
value[:uri] = Utils.URI(uri)
end
end
super(value)
end
memoized(:user) { uri.user && Utils.unescape(uri.user) }
memoized(:password) { uri.password && Utils.unescape(uri.password) }
end
class ConnectionOptions < Options.new(:request, :proxy, :ssl, :builder, :url,
:parallel_manager, :params, :headers, :builder_class)
options :request => RequestOptions, :ssl => SSLOptions
memoized(:request) { self.class.options_for(:request).new }
memoized(:ssl) { self.class.options_for(:ssl).new }
memoized(:builder_class) { RackBuilder }
def new_builder(block)
builder_class.new(&block)
end
end
class Env < Options.new(:method, :body, :url, :request, :request_headers,
:ssl, :parallel_manager, :params, :response, :response_headers, :status)
ContentLength = 'Content-Length'.freeze
StatusesWithoutBody = Set.new [204, 304]
SuccessfulStatuses = 200..299
# A Set of HTTP verbs that typically send a body. If no body is set for
# these requests, the Content-Length header is set to 0.
MethodsWithBodies = Set.new [:post, :put, :patch, :options]
options :request => RequestOptions,
:request_headers => Utils::Headers, :response_headers => Utils::Headers
extend Forwardable
def_delegators :request, :params_encoder
# Public
def self.from(value)
env = super(value)
if value.respond_to?(:custom_members)
env.custom_members.update(value.custom_members)
end
env
end
# Public
def [](key)
if in_member_set?(key)
super(key)
else
custom_members[key]
end
end
# Public
def []=(key, value)
if in_member_set?(key)
super(key, value)
else
custom_members[key] = value
end
end
# Public
def success?
SuccessfulStatuses.include?(status)
end
# Public
def needs_body?
!body && MethodsWithBodies.include?(method)
end
# Public
def clear_body
request_headers[ContentLength] = '0'
self.body = ''
end
# Public
def parse_body?
!StatusesWithoutBody.include?(status)
end
# Public
def parallel?
!!parallel_manager
end
def inspect
attrs = [nil]
members.each do |mem|
if value = send(mem)
attrs << "@#{mem}=#{value.inspect}"
end
end
if !custom_members.empty?
attrs << "@custom=#{custom_members.inspect}"
end
%(#<#{self.class}#{attrs.join(" ")}>)
end
# Internal
def custom_members
@custom_members ||= {}
end
# Internal
if members.first.is_a?(Symbol)
def in_member_set?(key)
self.class.member_set.include?(key.to_sym)
end
else
def in_member_set?(key)
self.class.member_set.include?(key.to_s)
end
end
# Internal
def self.member_set
@member_set ||= Set.new(members)
end
end
end

@ -0,0 +1,197 @@
require "forwardable"
module Faraday
module NestedParamsEncoder
class << self
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
def self.encode(params)
return nil if params == nil
if !params.is_a?(Array)
if !params.respond_to?(:to_hash)
raise TypeError,
"Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.kind_of?(Symbol)
[key, value]
end
# Useful default for OAuth and caching.
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort!
end
# Helper lambda
to_query = lambda do |parent, value|
if value.is_a?(Hash)
value = value.map do |key, val|
key = escape(key)
[key, val]
end
value.sort!
buffer = ""
value.each do |key, val|
new_parent = "#{parent}%5B#{key}%5D"
buffer << "#{to_query.call(new_parent, val)}&"
end
return buffer.chop
elsif value.is_a?(Array)
buffer = ""
value.each_with_index do |val, i|
new_parent = "#{parent}%5B%5D"
buffer << "#{to_query.call(new_parent, val)}&"
end
return buffer.chop
elsif value.nil?
return parent
else
encoded_value = escape(value)
return "#{parent}=#{encoded_value}"
end
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = ''
params.each do |parent, value|
encoded_parent = escape(parent)
buffer << "#{to_query.call(encoded_parent, value)}&"
end
return buffer.chop
end
def self.decode(query)
return nil if query == nil
params = {}
query.split("&").each do |pair|
next if pair.empty?
key, value = pair.split("=", 2)
key = unescape(key)
value = unescape(value.gsub(/\+/, ' ')) if value
subkeys = key.scan(/[^\[\]]+(?:\]?\[\])?/)
context = params
subkeys.each_with_index do |subkey, i|
is_array = subkey =~ /[\[\]]+\Z/
subkey = $` if is_array
last_subkey = i == subkeys.length - 1
if !last_subkey || is_array
value_type = is_array ? Array : Hash
if context[subkey] && !context[subkey].is_a?(value_type)
raise TypeError, "expected %s (got %s) for param `%s'" % [
value_type.name,
context[subkey].class.name,
subkey
]
end
context = (context[subkey] ||= value_type.new)
end
if context.is_a?(Array) && !is_array
if !context.last.is_a?(Hash) || context.last.has_key?(subkey)
context << {}
end
context = context.last
end
if last_subkey
if is_array
context << value
else
context[subkey] = value
end
end
end
end
dehash(params, 0)
end
# Internal: convert a nested hash with purely numeric keys into an array.
# FIXME: this is not compatible with Rack::Utils.parse_nested_query
def self.dehash(hash, depth)
hash.each do |key, value|
hash[key] = dehash(value, depth + 1) if value.kind_of?(Hash)
end
if depth > 0 && !hash.empty? && hash.keys.all? { |k| k =~ /^\d+$/ }
hash.keys.sort.inject([]) { |all, key| all << hash[key] }
else
hash
end
end
end
module FlatParamsEncoder
class << self
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
def self.encode(params)
return nil if params == nil
if !params.is_a?(Array)
if !params.respond_to?(:to_hash)
raise TypeError,
"Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.kind_of?(Symbol)
[key, value]
end
# Useful default for OAuth and caching.
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort!
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = ''
params.each do |key, value|
encoded_key = escape(key)
value = value.to_s if value == true || value == false
if value == nil
buffer << "#{encoded_key}&"
elsif value.kind_of?(Array)
value.each do |sub_value|
encoded_value = escape(sub_value)
buffer << "#{encoded_key}=#{encoded_value}&"
end
else
encoded_value = escape(value)
buffer << "#{encoded_key}=#{encoded_value}&"
end
end
return buffer.chop
end
def self.decode(query)
empty_accumulator = {}
return nil if query == nil
split_query = (query.split('&').map do |pair|
pair.split('=', 2) if pair && !pair.empty?
end).compact
return split_query.inject(empty_accumulator.dup) do |accu, pair|
pair[0] = unescape(pair[0])
pair[1] = true if pair[1].nil?
if pair[1].respond_to?(:to_str)
pair[1] = unescape(pair[1].to_str.gsub(/\+/, " "))
end
if accu[pair[0]].kind_of?(Array)
accu[pair[0]] << pair[1]
elsif accu[pair[0]]
accu[pair[0]] = [accu[pair[0]], pair[1]]
else
accu[pair[0]] = pair[1]
end
accu
end
end
end
end

@ -0,0 +1,213 @@
module Faraday
# A Builder that processes requests into responses by passing through an inner
# middleware stack (heavily inspired by Rack).
#
# Faraday::Connection.new(:url => 'http://sushi.com') do |builder|
# builder.request :url_encoded # Faraday::Request::UrlEncoded
# builder.adapter :net_http # Faraday::Adapter::NetHttp
# end
class RackBuilder
attr_accessor :handlers
# Error raised when trying to modify the stack after calling `lock!`
class StackLocked < RuntimeError; end
# borrowed from ActiveSupport::Dependencies::Reference &
# ActionDispatch::MiddlewareStack::Middleware
class Handler
@@constants_mutex = Mutex.new
@@constants = Hash.new { |h, k|
value = k.respond_to?(:constantize) ? k.constantize : Object.const_get(k)
@@constants_mutex.synchronize { h[k] = value }
}
attr_reader :name
def initialize(klass, *args, &block)
@name = klass.to_s
if klass.respond_to?(:name)
@@constants_mutex.synchronize { @@constants[@name] = klass }
end
@args, @block = args, block
end
def klass() @@constants[@name] end
def inspect() @name end
def ==(other)
if other.is_a? Handler
self.name == other.name
elsif other.respond_to? :name
klass == other
else
@name == other.to_s
end
end
def build(app)
klass.new(app, *@args, &@block)
end
end
def initialize(handlers = [])
@handlers = handlers
if block_given?
build(&Proc.new)
elsif @handlers.empty?
# default stack, if nothing else is configured
self.request :url_encoded
self.adapter Faraday.default_adapter
end
end
def build(options = {})
raise_if_locked
@handlers.clear unless options[:keep]
yield(self) if block_given?
end
def [](idx)
@handlers[idx]
end
# Locks the middleware stack to ensure no further modifications are possible.
def lock!
@handlers.freeze
end
def locked?
@handlers.frozen?
end
def use(klass, *args, &block)
if klass.is_a? Symbol
use_symbol(Faraday::Middleware, klass, *args, &block)
else
raise_if_locked
@handlers << self.class::Handler.new(klass, *args, &block)
end
end
def request(key, *args, &block)
use_symbol(Faraday::Request, key, *args, &block)
end
def response(key, *args, &block)
use_symbol(Faraday::Response, key, *args, &block)
end
def adapter(key, *args, &block)
use_symbol(Faraday::Adapter, key, *args, &block)
end
## methods to push onto the various positions in the stack:
def insert(index, *args, &block)
raise_if_locked
index = assert_index(index)
handler = self.class::Handler.new(*args, &block)
@handlers.insert(index, handler)
end
alias_method :insert_before, :insert
def insert_after(index, *args, &block)
index = assert_index(index)
insert(index + 1, *args, &block)
end
def swap(index, *args, &block)
raise_if_locked
index = assert_index(index)
@handlers.delete_at(index)
insert(index, *args, &block)
end
def delete(handler)
raise_if_locked
@handlers.delete(handler)
end
# Processes a Request into a Response by passing it through this Builder's
# middleware stack.
#
# connection - Faraday::Connection
# request - Faraday::Request
#
# Returns a Faraday::Response.
def build_response(connection, request)
app.call(build_env(connection, request))
end
# The "rack app" wrapped in middleware. All requests are sent here.
#
# The builder is responsible for creating the app object. After this,
# the builder gets locked to ensure no further modifications are made
# to the middleware stack.
#
# Returns an object that responds to `call` and returns a Response.
def app
@app ||= begin
lock!
to_app(lambda { |env|
response = Response.new
env.response = response
response.finish(env) unless env.parallel?
response
})
end
end
def to_app(inner_app)
# last added handler is the deepest and thus closest to the inner app
@handlers.reverse.inject(inner_app) { |app, handler| handler.build(app) }
end
def ==(other)
other.is_a?(self.class) && @handlers == other.handlers
end
def dup
self.class.new(@handlers.dup)
end
# ENV Keys
# :method - a symbolized request method (:get, :post)
# :body - the request body that will eventually be converted to a string.
# :url - URI instance for the current request.
# :status - HTTP response status code
# :request_headers - hash of HTTP Headers to be sent to the server
# :response_headers - Hash of HTTP headers from the server
# :parallel_manager - sent if the connection is in parallel mode
# :request - Hash of options for configuring the request.
# :timeout - open/read timeout Integer in seconds
# :open_timeout - read timeout Integer in seconds
# :proxy - Hash of proxy options
# :uri - Proxy Server URI
# :user - Proxy server username
# :password - Proxy server password
# :ssl - Hash of options for configuring SSL requests.
def build_env(connection, request)
Env.new(request.method, request.body,
connection.build_exclusive_url(request.path, request.params, request.options.params_encoder),
request.options, request.headers, connection.ssl,
connection.parallel_manager)
end
private
def raise_if_locked
raise StackLocked, "can't modify middleware stack after making a request" if locked?
end
def use_symbol(mod, key, *args, &block)
use(mod.lookup_middleware(key), *args, &block)
end
def assert_index(index)
idx = index.is_a?(Integer) ? index : @handlers.index(index)
raise "No such handler: #{index.inspect}" unless idx
idx
end
end
end

@ -0,0 +1,92 @@
module Faraday
# Used to setup urls, params, headers, and the request body in a sane manner.
#
# @connection.post do |req|
# req.url 'http://localhost', 'a' => '1' # 'http://localhost?a=1'
# req.headers['b'] = '2' # Header
# req.params['c'] = '3' # GET Param
# req['b'] = '2' # also Header
# req.body = 'abc'
# end
#
class Request < Struct.new(:method, :path, :params, :headers, :body, :options)
extend MiddlewareRegistry
register_middleware File.expand_path('../request', __FILE__),
:url_encoded => [:UrlEncoded, 'url_encoded'],
:multipart => [:Multipart, 'multipart'],
:retry => [:Retry, 'retry'],
:authorization => [:Authorization, 'authorization'],
:basic_auth => [:BasicAuthentication, 'basic_authentication'],
:token_auth => [:TokenAuthentication, 'token_authentication'],
:instrumentation => [:Instrumentation, 'instrumentation']
def self.create(request_method)
new(request_method).tap do |request|
yield(request) if block_given?
end
end
# Public: Replace params, preserving the existing hash type
def params=(hash)
if params
params.replace hash
else
super
end
end
# Public: Replace request headers, preserving the existing hash type
def headers=(hash)
if headers
headers.replace hash
else
super
end
end
def url(path, params = nil)
if path.respond_to? :query
if query = path.query
path = path.dup
path.query = nil
end
else
path, query = path.split('?', 2)
end
self.path = path
self.params.merge_query query, options.params_encoder
self.params.update(params) if params
end
def [](key)
headers[key]
end
def []=(key, value)
headers[key] = value
end
# ENV Keys
# :method - a symbolized request method (:get, :post)
# :body - the request body that will eventually be converted to a string.
# :url - URI instance for the current request.
# :status - HTTP response status code
# :request_headers - hash of HTTP Headers to be sent to the server
# :response_headers - Hash of HTTP headers from the server
# :parallel_manager - sent if the connection is in parallel mode
# :request - Hash of options for configuring the request.
# :timeout - open/read timeout Integer in seconds
# :open_timeout - read timeout Integer in seconds
# :proxy - Hash of proxy options
# :uri - Proxy Server URI
# :user - Proxy server username
# :password - Proxy server password
# :ssl - Hash of options for configuring SSL requests.
def to_env(connection)
Env.new(method, body, connection.build_exclusive_url(path, params),
options, headers, connection.ssl, connection.parallel_manager)
end
end
end

@ -0,0 +1,42 @@
module Faraday
class Request::Authorization < Faraday::Middleware
KEY = "Authorization".freeze unless defined? KEY
# Public
def self.header(type, token)
case token
when String, Symbol
"#{type} #{token}"
when Hash
build_hash(type.to_s, token)
else
raise ArgumentError, "Can't build an Authorization #{type} header from #{token.inspect}"
end
end
# Internal
def self.build_hash(type, hash)
offset = KEY.size + type.size + 3
comma = ",\n#{' ' * offset}"
values = []
hash.each do |key, value|
values << "#{key}=#{value.to_s.inspect}"
end
"#{type} #{values * comma}"
end
def initialize(app, type, token)
@header_value = self.class.header(type, token)
super(app)
end
# Public
def call(env)
unless env.request_headers[KEY]
env.request_headers[KEY] = @header_value
end
@app.call(env)
end
end
end

@ -0,0 +1,13 @@
require 'base64'
module Faraday
class Request::BasicAuthentication < Request.load_middleware(:authorization)
# Public
def self.header(login, pass)
value = Base64.encode64([login, pass].join(':'))
value.gsub!("\n", '')
super(:Basic, value)
end
end
end

@ -0,0 +1,36 @@
module Faraday
class Request::Instrumentation < Faraday::Middleware
class Options < Faraday::Options.new(:name, :instrumenter)
def name
self[:name] ||= 'request.faraday'
end
def instrumenter
self[:instrumenter] ||= ActiveSupport::Notifications
end
end
# Public: Instruments requests using Active Support.
#
# Measures time spent only for synchronous requests.
#
# Examples
#
# ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env|
# url = env[:url]
# http_method = env[:method].to_s.upcase
# duration = ends - starts
# $stderr.puts '[%s] %s %s (%.3f s)' % [url.host, http_method, url.request_uri, duration]
# end
def initialize(app, options = nil)
super(app)
@name, @instrumenter = Options.from(options).values_at(:name, :instrumenter)
end
def call(env)
@instrumenter.instrument(@name, env) do
@app.call(env)
end
end
end
end

@ -0,0 +1,63 @@
require File.expand_path("../url_encoded", __FILE__)
module Faraday
class Request::Multipart < Request::UrlEncoded
self.mime_type = 'multipart/form-data'.freeze
DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze unless defined? DEFAULT_BOUNDARY
def call(env)
match_content_type(env) do |params|
env.request.boundary ||= DEFAULT_BOUNDARY
env.request_headers[CONTENT_TYPE] += "; boundary=#{env.request.boundary}"
env.body = create_multipart(env, params)
end
@app.call env
end
def process_request?(env)
type = request_type(env)
env.body.respond_to?(:each_key) and !env.body.empty? and (
(type.empty? and has_multipart?(env.body)) or
type == self.class.mime_type
)
end
def has_multipart?(obj)
# string is an enum in 1.8, returning list of itself
if obj.respond_to?(:each) && !obj.is_a?(String)
(obj.respond_to?(:values) ? obj.values : obj).each do |val|
return true if (val.respond_to?(:content_type) || has_multipart?(val))
end
end
false
end
def create_multipart(env, params)
boundary = env.request.boundary
parts = process_params(params) do |key, value|
Faraday::Parts::Part.new(boundary, key, value)
end
parts << Faraday::Parts::EpiloguePart.new(boundary)
body = Faraday::CompositeReadIO.new(parts)
env.request_headers[Faraday::Env::ContentLength] = body.length.to_s
return body
end
def process_params(params, prefix = nil, pieces = nil, &block)
params.inject(pieces || []) do |all, (key, value)|
key = "#{prefix}[#{key}]" if prefix
case value
when Array
values = value.inject([]) { |a,v| a << [nil, v] }
process_params(values, key, all, &block)
when Hash
process_params(value, key, all, &block)
else
all << block.call(key, value)
end
end
end
end
end

@ -0,0 +1,154 @@
module Faraday
# Catches exceptions and retries each request a limited number of times.
#
# By default, it retries 2 times and handles only timeout exceptions. It can
# be configured with an arbitrary number of retries, a list of exceptions to
# handle, a retry interval, a percentage of randomness to add to the retry
# interval, and a backoff factor.
#
# Examples
#
# Faraday.new do |conn|
# conn.request :retry, max: 2, interval: 0.05,
# interval_randomness: 0.5, backoff_factor: 2
# exceptions: [CustomException, 'Timeout::Error']
# conn.adapter ...
# end
#
# This example will result in a first interval that is random between 0.05 and 0.075 and a second
# interval that is random between 0.1 and 0.15
#
class Request::Retry < Faraday::Middleware
IDEMPOTENT_METHODS = [:delete, :get, :head, :options, :put]
class Options < Faraday::Options.new(:max, :interval, :max_interval, :interval_randomness,
:backoff_factor, :exceptions, :methods, :retry_if)
DEFAULT_CHECK = lambda { |env,exception| false }
def self.from(value)
if Fixnum === value
new(value)
else
super(value)
end
end
def max
(self[:max] ||= 2).to_i
end
def interval
(self[:interval] ||= 0).to_f
end
def max_interval
(self[:max_interval] ||= Float::MAX).to_f
end
def interval_randomness
(self[:interval_randomness] ||= 0).to_f
end
def backoff_factor
(self[:backoff_factor] ||= 1).to_f
end
def exceptions
Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error',
Error::TimeoutError])
end
def methods
Array(self[:methods] ||= IDEMPOTENT_METHODS)
end
def retry_if
self[:retry_if] ||= DEFAULT_CHECK
end
end
# Public: Initialize middleware
#
# Options:
# max - Maximum number of retries (default: 2)
# interval - Pause in seconds between retries (default: 0)
# interval_randomness - The maximum random interval amount expressed
# as a float between 0 and 1 to use in addition to the
# interval. (default: 0)
# max_interval - An upper limit for the interval (default: Float::MAX)
# backoff_factor - The amount to multiple each successive retry's
# interval amount by in order to provide backoff
# (default: 1)
# exceptions - The list of exceptions to handle. Exceptions can be
# given as Class, Module, or String. (default:
# [Errno::ETIMEDOUT, Timeout::Error,
# Error::TimeoutError])
# methods - A list of HTTP methods to retry without calling retry_if. Pass
# an empty Array to call retry_if for all exceptions.
# (defaults to the idempotent HTTP methods in IDEMPOTENT_METHODS)
# retry_if - block that will receive the env object and the exception raised
# and should decide if the code should retry still the action or
# not independent of the retry count. This would be useful
# if the exception produced is non-recoverable or if the
# the HTTP method called is not idempotent.
# (defaults to return false)
def initialize(app, options = nil)
super(app)
@options = Options.from(options)
@errmatch = build_exception_matcher(@options.exceptions)
end
def sleep_amount(retries)
retry_index = @options.max - retries
current_interval = @options.interval * (@options.backoff_factor ** retry_index)
current_interval = [current_interval, @options.max_interval].min
random_interval = rand * @options.interval_randomness.to_f * @options.interval
current_interval + random_interval
end
def call(env)
retries = @options.max
request_body = env[:body]
begin
env[:body] = request_body # after failure env[:body] is set to the response body
@app.call(env)
rescue @errmatch => exception
if retries > 0 && retry_request?(env, exception)
retries -= 1
sleep sleep_amount(retries + 1)
retry
end
raise
end
end
# Private: construct an exception matcher object.
#
# An exception matcher for the rescue clause can usually be any object that
# responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
def build_exception_matcher(exceptions)
matcher = Module.new
(class << matcher; self; end).class_eval do
define_method(:===) do |error|
exceptions.any? do |ex|
if ex.is_a? Module
error.is_a? ex
else
error.class.to_s == ex.to_s
end
end
end
end
matcher
end
private
def retry_request?(env, exception)
@options.methods.include?(env[:method]) || @options.retry_if.call(env, exception)
end
end
end

@ -0,0 +1,15 @@
module Faraday
class Request::TokenAuthentication < Request.load_middleware(:authorization)
# Public
def self.header(token, options = nil)
options ||= {}
options[:token] = token
super(:Token, options)
end
def initialize(app, token, options = nil)
super(app, token, options)
end
end
end

@ -0,0 +1,36 @@
module Faraday
class Request::UrlEncoded < Faraday::Middleware
CONTENT_TYPE = 'Content-Type'.freeze unless defined? CONTENT_TYPE
class << self
attr_accessor :mime_type
end
self.mime_type = 'application/x-www-form-urlencoded'.freeze
def call(env)
match_content_type(env) do |data|
params = Faraday::Utils::ParamsHash[data]
env.body = params.to_query(env.params_encoder)
end
@app.call env
end
def match_content_type(env)
if process_request?(env)
env.request_headers[CONTENT_TYPE] ||= self.class.mime_type
yield(env.body) unless env.body.respond_to?(:to_str)
end
end
def process_request?(env)
type = request_type(env)
env.body and (type.empty? or type == self.class.mime_type)
end
def request_type(env)
type = env.request_headers[CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end

@ -0,0 +1,93 @@
require 'forwardable'
module Faraday
class Response
# Used for simple response middleware.
class Middleware < Faraday::Middleware
def call(env)
@app.call(env).on_complete do |environment|
on_complete(environment)
end
end
# Override this to modify the environment after the response has finished.
# Calls the `parse` method if defined
def on_complete(env)
env.body = parse(env.body) if respond_to?(:parse) && env.parse_body?
end
end
extend Forwardable
extend MiddlewareRegistry
register_middleware File.expand_path('../response', __FILE__),
:raise_error => [:RaiseError, 'raise_error'],
:logger => [:Logger, 'logger']
def initialize(env = nil)
@env = Env.from(env) if env
@on_complete_callbacks = []
end
attr_reader :env
def_delegators :env, :to_hash
def status
finished? ? env.status : nil
end
def headers
finished? ? env.response_headers : {}
end
def_delegator :headers, :[]
def body
finished? ? env.body : nil
end
def finished?
!!env
end
def on_complete
if not finished?
@on_complete_callbacks << Proc.new
else
yield(env)
end
return self
end
def finish(env)
raise "response already finished" if finished?
@env = env.is_a?(Env) ? env : Env.from(env)
@on_complete_callbacks.each { |callback| callback.call(@env) }
return self
end
def success?
finished? && env.success?
end
# because @on_complete_callbacks cannot be marshalled
def marshal_dump
!finished? ? nil : {
:status => @env.status, :body => @env.body,
:response_headers => @env.response_headers
}
end
def marshal_load(env)
@env = Env.from(env)
end
# Expand the env with more properties, without overriding existing ones.
# Useful for applying request params after restoring a marshalled Response.
def apply_request(request_env)
raise "response didn't finish yet" unless finished?
@env = Env.from(request_env).update(@env)
return self
end
end
end

@ -0,0 +1,59 @@
require 'forwardable'
module Faraday
class Response::Logger < Response::Middleware
extend Forwardable
DEFAULT_OPTIONS = { :bodies => false }
def initialize(app, logger = nil, options = {})
super(app)
@logger = logger || begin
require 'logger'
::Logger.new(STDOUT)
end
@options = DEFAULT_OPTIONS.merge(options)
end
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
def call(env)
info "#{env.method} #{env.url.to_s}"
debug('request') { dump_headers env.request_headers }
debug('request') { dump_body(env[:body]) } if env[:body] && log_body?(:request)
super
end
def on_complete(env)
info('Status') { env.status.to_s }
debug('response') { dump_headers env.response_headers }
debug('response') { dump_body env[:body] } if env[:body] && log_body?(:response)
end
private
def dump_headers(headers)
headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
end
def dump_body(body)
if body.respond_to?(:to_str)
body.to_str
else
pretty_inspect(body)
end
end
def pretty_inspect(body)
require 'pp' unless body.respond_to?(:pretty_inspect)
body.pretty_inspect
end
def log_body?(type)
case @options[:bodies]
when Hash then @options[:bodies][type]
else @options[:bodies]
end
end
end
end

@ -0,0 +1,21 @@
module Faraday
class Response::RaiseError < Response::Middleware
ClientErrorStatuses = 400...600
def on_complete(env)
case env[:status]
when 404
raise Faraday::Error::ResourceNotFound, response_values(env)
when 407
# mimic the behavior that we get with proxy requests with HTTPS
raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
when ClientErrorStatuses
raise Faraday::Error::ClientError, response_values(env)
end
end
def response_values(env)
{:status => env.status, :headers => env.response_headers, :body => env.body}
end
end
end

@ -0,0 +1,67 @@
begin
require 'composite_io'
require 'parts'
require 'stringio'
rescue LoadError
$stderr.puts "Install the multipart-post gem."
raise
end
module Faraday
# Similar but not compatible with ::CompositeReadIO provided by multipart-post.
class CompositeReadIO
def initialize(*parts)
@parts = parts.flatten
@ios = @parts.map { |part| part.to_io }
@index = 0
end
def length
@parts.inject(0) { |sum, part| sum + part.length }
end
def rewind
@ios.each { |io| io.rewind }
@index = 0
end
# Read from IOs in order until `length` bytes have been received.
def read(length = nil, outbuf = nil)
got_result = false
outbuf = outbuf ? outbuf.replace("") : ""
while io = current_io
if result = io.read(length)
got_result ||= !result.nil?
result.force_encoding("BINARY") if result.respond_to?(:force_encoding)
outbuf << result
length -= result.length if length
break if length == 0
end
advance_io
end
(!got_result && length) ? nil : outbuf
end
def close
@ios.each { |io| io.close }
end
def ensure_open_and_readable
# Rubinius compatibility
end
private
def current_io
@ios[@index]
end
def advance_io
@index += 1
end
end
UploadIO = ::UploadIO
Parts = ::Parts
end

@ -0,0 +1,309 @@
require 'thread'
module Faraday
module Utils
extend self
# Adapted from Rack::Utils::HeaderHash
class Headers < ::Hash
def self.from(value)
new(value)
end
def initialize(hash = nil)
super()
@names = {}
self.update(hash || {})
end
# on dup/clone, we need to duplicate @names hash
def initialize_copy(other)
super
@names = other.names.dup
end
# need to synchronize concurrent writes to the shared KeyMap
keymap_mutex = Mutex.new
# symbol -> string mapper + cache
KeyMap = Hash.new do |map, key|
value = if key.respond_to?(:to_str)
key
else
key.to_s.split('_'). # :user_agent => %w(user agent)
each { |w| w.capitalize! }. # => %w(User Agent)
join('-') # => "User-Agent"
end
keymap_mutex.synchronize { map[key] = value }
end
KeyMap[:etag] = "ETag"
def [](k)
k = KeyMap[k]
super(k) || super(@names[k.downcase])
end
def []=(k, v)
k = KeyMap[k]
k = (@names[k.downcase] ||= k)
# join multiple values with a comma
v = v.to_ary.join(', ') if v.respond_to? :to_ary
super(k, v)
end
def fetch(k, *args, &block)
k = KeyMap[k]
key = @names.fetch(k.downcase, k)
super(key, *args, &block)
end
def delete(k)
k = KeyMap[k]
if k = @names[k.downcase]
@names.delete k.downcase
super(k)
end
end
def include?(k)
@names.include? k.downcase
end
alias_method :has_key?, :include?
alias_method :member?, :include?
alias_method :key?, :include?
def merge!(other)
other.each { |k, v| self[k] = v }
self
end
alias_method :update, :merge!
def merge(other)
hash = dup
hash.merge! other
end
def replace(other)
clear
@names.clear
self.update other
self
end
def to_hash() ::Hash.new.update(self) end
def parse(header_string)
return unless header_string && !header_string.empty?
header_string.split(/\r\n/).
tap { |a| a.shift if a.first.index('HTTP/') == 0 }. # drop the HTTP status line
map { |h| h.split(/:\s+/, 2) }.reject { |p| p[0].nil? }. # split key and value, ignore blank lines
each { |key, value|
# join multiple values with a comma
if self[key]
self[key] << ', ' << value
else
self[key] = value
end
}
end
protected
def names
@names
end
end
# hash with stringified keys
class ParamsHash < Hash
def [](key)
super(convert_key(key))
end
def []=(key, value)
super(convert_key(key), value)
end
def delete(key)
super(convert_key(key))
end
def include?(key)
super(convert_key(key))
end
alias_method :has_key?, :include?
alias_method :member?, :include?
alias_method :key?, :include?
def update(params)
params.each do |key, value|
self[key] = value
end
self
end
alias_method :merge!, :update
def merge(params)
dup.update(params)
end
def replace(other)
clear
update(other)
end
def merge_query(query, encoder = nil)
if query && !query.empty?
update((encoder || Utils.default_params_encoder).decode(query))
end
self
end
def to_query(encoder = nil)
(encoder || Utils.default_params_encoder).encode(self)
end
private
def convert_key(key)
key.to_s
end
end
def build_query(params)
FlatParamsEncoder.encode(params)
end
def build_nested_query(params)
NestedParamsEncoder.encode(params)
end
ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
def escape(s)
s.to_s.gsub(ESCAPE_RE) {|match|
'%' + match.unpack('H2' * match.bytesize).join('%').upcase
}.tr(' ', '+')
end
def unescape(s) CGI.unescape s.to_s end
DEFAULT_SEP = /[&;] */n
# Adapted from Rack
def parse_query(query)
FlatParamsEncoder.decode(query)
end
def parse_nested_query(query)
NestedParamsEncoder.decode(query)
end
def default_params_encoder
@default_params_encoder ||= NestedParamsEncoder
end
class << self
attr_writer :default_params_encoder
end
# Stolen from Rack
def normalize_params(params, name, v = nil)
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''
return if k.empty?
if after == ""
if params[k]
params[k] = Array[params[k]] unless params[k].kind_of?(Array)
params[k] << v
else
params[k] = v
end
elsif after == "[]"
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
normalize_params(params[k].last, child_key, v)
else
params[k] << normalize_params({}, child_key, v)
end
else
params[k] ||= {}
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_params(params[k], after, v)
end
return params
end
# Normalize URI() behavior across Ruby versions
#
# url - A String or URI.
#
# Returns a parsed URI.
def URI(url)
if url.respond_to?(:host)
url
elsif url.respond_to?(:to_str)
default_uri_parser.call(url)
else
raise ArgumentError, "bad argument (expected URI object or URI string)"
end
end
def default_uri_parser
@default_uri_parser ||= begin
require 'uri'
Kernel.method(:URI)
end
end
def default_uri_parser=(parser)
@default_uri_parser = if parser.respond_to?(:call) || parser.nil?
parser
else
parser.method(:parse)
end
end
# Receives a String or URI and returns just the path with the query string sorted.
def normalize_path(url)
url = URI(url)
(url.path.start_with?('/') ? url.path : '/' + url.path) +
(url.query ? "?#{sort_query_params(url.query)}" : "")
end
# Recursive hash update
def deep_merge!(target, hash)
hash.each do |key, value|
if Hash === value and Hash === target[key]
target[key] = deep_merge(target[key], value)
else
target[key] = value
end
end
target
end
# Recursive hash merge
def deep_merge(source, hash)
deep_merge!(source.dup, hash)
end
protected
def sort_query_params(query)
query.split('&').sort.join('&')
end
end
end

@ -0,0 +1,14 @@
source 'https://rubygems.org'
gemspec
platforms :mri_19 do
gem 'ruby-debug19'
end
platforms :mri_18 do
gem 'ruby-debug'
end
group :development, :test do
gem 'rake'
end

@ -0,0 +1,60 @@
=== 2.0.0 / 2013-12-21
- Drop Ruby 1.8 compatibility
- GH #21: Fix FilePart length calculation for Ruby 1.9 when filename contains
multibyte characters (hexfet)
- GH #20: Ensure upload responds to both #content_type and #original_filename
(Steven Davidovitz)
- GH #31: Support setting headers on any part of the request (Socrates Vicente)
- GH #30: Support array values for params (Gustav Ernberg)
- GH #32: Fix respond_to? signature (Leo Cassarani)
- GH #33: Update README to markdown (Jagtesh Chadha)
- GH #35: Improved handling of array-type parameters (Steffen Grunwald)
=== 1.2.0 / 2013-02-25
- #25: Ruby 2 compatibility (thanks mislav)
=== 1.1.5 / 2012-02-12
- Fix length/bytesize of parts in 1.9 (#7, #14) (Jason Moore)
- Allow CompositeIO objects to be re-read by rewinding, like other IO
objects. (Luke Redpath)
=== 1.1.4 / 2011-11-23
- Non-functional changes in release (switch to Bundler gem tasks)
=== 1.1.3 / 2011-07-25
- More configurable header specification for parts (Gerrit Riessen)
=== 1.1.2 / 2011-05-24
- Fix CRLF file part miscalculation (Johannes Wagener)
- Fix Epilogue CRLF issue (suggestion by Neil Spring)
=== 1.1.1 / 2011-05-13
- GH# 9: Fixed Ruby 1.9.2 StringIO bug (thanks Alex Koppel)
=== 1.1.0 / 2011-01-11
- API CHANGE: UploadIO.convert! removed in favor of UploadIO.new
(Jeff Hodges)
=== 1.0.1 / 2010-04-27
- Doc updates, make gemspec based on more modern Rubygems
=== 1.0 / 2009-02-12
- Many fixes from mlooney, seems to work now. Putting the 0.9 seal of
approval on it.
=== 0.1 / 2008-08-12
* 1 major enhancement
* Birthday!

@ -0,0 +1,9 @@
lib/composite_io.rb
lib/multipartable.rb
lib/parts.rb
lib/net/http/post/multipart.rb
Manifest.txt
Rakefile
README.txt
test/test_composite_io.rb
test/net/http/post/test_multipart.rb

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save