Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
Code extracted from snoo and rewritten to be platform agnostic
  • Loading branch information
paradox460 committed Apr 5, 2016
0 parents commit eeed330
Show file tree
Hide file tree
Showing 18 changed files with 414 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
vendor
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--format documentation
--color
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AllCops:
TargetRubyVersion: 2.2
Metrics/LineLength:
Enabled: false
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
language: ruby
rvm:
- 2.3.0
before_install: gem install bundler -v 1.11.2
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in http_api_builder.gemspec
gemspec
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 Jeff Sandberg

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.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# HttpApiBuilder
A simple tool for building API clients that use HTTP.

This is for **clients** as in *consumers*, not for servers. Look into things like Rails-api or Grape for those.

[![Code Climate](https://codeclimate.com/github/paradox460/http_api_builder/badges/gpa.svg)](https://codeclimate.com/github/paradox460/http_api_builder)

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'http_api_builder'
```

And then execute:

$ bundle

Or install it yourself as:

$ gem install http_api_builder

## Usage

```ruby
require 'http_api_builder/client/http_rb'

class ElGoog < HttpApiBuilder::BaseClient
include HttpApiBuilder::Client::HttpRb

base_url 'https://google.com'

get '/', as: :search, params: {required: :q}
end
```

You can then use the API as such:

```ruby
g = ElGoog.new

g.search(q: 'ruby')
```

See the wiki for more details.

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).


```
The MIT License (MIT)
Copyright (c) 2016 Jeff Sandberg
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.
```
6 changes: 6 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task default: :spec
14 changes: 14 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env ruby

require 'bundler/setup'
require 'http_api_builder'

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start

require 'pry'
Pry.start
8 changes: 8 additions & 0 deletions bin/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx

bundle install

# Do any other automated setup that you need to do here
31 changes: 31 additions & 0 deletions http_api_builder.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'http_api_builder/version'

Gem::Specification.new do |spec|
spec.name = 'http_api_builder'
spec.version = HttpApiBuilder::VERSION
spec.authors = ['Jeff Sandberg']
spec.email = ['[email protected]']

spec.summary = 'A utility gem providing a DSL for building HTTP api wrappers.'
spec.description = 'A gem providing a nice DSL for building HTTP api wrappers.'
spec.homepage = 'https://github.com/paradox460/http_api_builder'
spec.license = 'MIT'

spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.required_ruby_version = ">= 2.2"

spec.add_development_dependency 'bundler', '~> 1.11'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'pry-stack_explorer'
spec.add_development_dependency 'pry-byebug'
spec.add_development_dependency 'http'
end
34 changes: 34 additions & 0 deletions lib/http_api_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'http_api_builder/version'
require 'http_api_builder/dsl'
require 'http_api_builder/helpers'

module HttpApiBuilder
# A basic HTTP client. Meant to be extended from.
class BaseClient
extend Dsl
include Helpers

def initialize(); end

# Perform the request, post processors, and return the result
def perform(method, path, form: nil, query: nil, body: nil, json: nil, &_block) # rubocop:disable Metrics/ParameterLists
response = request(method, path, form: form, query: query, body: body, json: json)
status = response.status
resource = response.body
block_given? ? yield(resource, status, response) : resource
end

# Placeholder for your request method.
# Accepts these params, for you to do whatever you like with. See the HTTPrb_client implementation
#
# @param [Symbol] method The HTTP VERB to use
# @param [String, URI] path The path, excluding base_url, which should be prepended inside your implementation
# @param [Hash] form: nil Form data, for encoding into HTTP form encoding
# @param [Hash] query: nil Query key/value pairs
# @param [String] body: nil A raw body
# @param [Hash, Array] json: nil Hash/Array data to be encoded as JSON.
def request(*)
raise 'HttpApiBuilder::BaseClient#request must be implemented, see documentation'
end
end
end
15 changes: 15 additions & 0 deletions lib/http_api_builder/client/http_rb.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'http'

module HttpApiBuilder
module Client
# A demonstration implementation, using HTTP.rb
# This is functional and pretty much production ready, but you can
# easily rewrite it to use curb or typhoeus or anything else really
module HttpRb
def request(verb, path, form:, query:, body:, json:) # rubocop:disable Metrics/ParameterLists
url = URI.join(self.class.base_url || '', path)
HTTP.send(verb, url, form: form, params: query, body: body, json: json)
end
end
end
end
95 changes: 95 additions & 0 deletions lib/http_api_builder/dsl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require 'forwardable'

module HttpApiBuilder
# Module for restful api dsl commands
module Dsl
VERBS =
%i(get head post put delete trace options connect) + # HTTP 1.1
%i(propfind proppatch mkcol copy move lock unlock) + # WebDAV
%i(orderpatch) + # WebDAV Ordered Collections protocol
%i(acl) + # WebDAV Access Control protocol
%i(patch) + # PATCH method for HTTP
%i(search) # WebDAV search

# Set the initial URL used by the gem
def base_url(value = nil)
value.nil? ? @base_url : (@base_url = value)
end

protected

# Generate whiny and quiet API consumer methods.
#
# Whiny methods have a bang suffix and raise errors if they fail
# Quiet methods do not have the bang suffix and return nil if they fail
#
# eg:
# endpoint '/path', as: :mymethod
# results in:
# mymethod! <-- whiny
# mymethod <-- quiet
def endpoint(path, as:, using: :get, params: nil, form: nil, body: nil, processors: nil, json: nil) # rubocop:disable Metrics/ParameterLists
def_whiny_method as, path, using, processors, params, form, body, json
def_quiet_method as
end

VERBS.each do |v|
define_method v do |path, **opts|
consume path, using: v, **opts
end
end

private

# Generate a consumer method that raises exceptions when requests raise an error
#
def def_whiny_method(name, path, using, processors, params, form, body, json) # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize, Metrics/MethodLength
required, optional = requirements(path, params)

define_method :"#{name}!" do |opts = {}|
validate_args! opts.keys, required, optional

reqpath = interpolate_path(path, opts)
query = query_params(path, opts, required, optional)

form = Hash(form).merge(Hash(opts[:form]))
json = opts[:json] || json
body = opts[:body] || body
perform(using, reqpath, form: form, query: query, body: body, json: json) do |resource, *_|
run_processors resource, processors
end
end
end

# Generate a consumer method that returns nil when requests raise errors
#
def def_quiet_method(name)
define_method name do |opts = {}|
begin
send(:"#{name}!", opts)
rescue StandardError
nil
end
end
end

# Extract param segments from a path, paperclip style. Returns an array of symbols matching the names of the param segments.
#
# Param segments are sections of the path that begin with `:`, and run to the next /
def interpolated_params(path)
path.split('/').reject { |i| i.length.zero? || i !~ /^:/ }.uniq.map { |i| i[1..-1].to_sym }
end

# Parse out and return the required and optional arguments
#
# Required are any that are in the URL or in the required hash
# Optional are any other arguments.
def requirements(path, params)
required, optional = Hash(params).values_at(*%i(required optional)).map { |list| Array(list) }

required += interpolated_params(path)

[required, optional]
end
end
end
Loading

0 comments on commit eeed330

Please sign in to comment.