extended_her 0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +2 -0
- data/LICENSE +7 -0
- data/README.md +723 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +32 -0
- data/examples/twitter-oauth/Gemfile +13 -0
- data/examples/twitter-oauth/app.rb +50 -0
- data/examples/twitter-oauth/config.ru +5 -0
- data/examples/twitter-oauth/views/index.haml +9 -0
- data/examples/twitter-search/Gemfile +12 -0
- data/examples/twitter-search/app.rb +55 -0
- data/examples/twitter-search/config.ru +5 -0
- data/examples/twitter-search/views/index.haml +9 -0
- data/extended_her.gemspec +27 -0
- data/lib/her.rb +23 -0
- data/lib/her/api.rb +108 -0
- data/lib/her/base.rb +17 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +5 -0
- data/lib/her/exceptions/exception.rb +4 -0
- data/lib/her/exceptions/record_invalid.rb +8 -0
- data/lib/her/exceptions/record_not_found.rb +13 -0
- data/lib/her/middleware.rb +9 -0
- data/lib/her/middleware/accept_json.rb +15 -0
- data/lib/her/middleware/first_level_parse_json.rb +34 -0
- data/lib/her/middleware/second_level_parse_json.rb +28 -0
- data/lib/her/model.rb +69 -0
- data/lib/her/model/base.rb +7 -0
- data/lib/her/model/hooks.rb +114 -0
- data/lib/her/model/http.rb +284 -0
- data/lib/her/model/introspection.rb +57 -0
- data/lib/her/model/orm.rb +191 -0
- data/lib/her/model/orm/comparison_methods.rb +20 -0
- data/lib/her/model/orm/create_methods.rb +29 -0
- data/lib/her/model/orm/destroy_methods.rb +53 -0
- data/lib/her/model/orm/error_methods.rb +19 -0
- data/lib/her/model/orm/fields_definition.rb +15 -0
- data/lib/her/model/orm/find_methods.rb +46 -0
- data/lib/her/model/orm/persistance_methods.rb +22 -0
- data/lib/her/model/orm/relation_mapper.rb +21 -0
- data/lib/her/model/orm/save_methods.rb +58 -0
- data/lib/her/model/orm/serialization_methods.rb +28 -0
- data/lib/her/model/orm/update_methods.rb +31 -0
- data/lib/her/model/paths.rb +82 -0
- data/lib/her/model/relationships.rb +191 -0
- data/lib/her/paginated_collection.rb +20 -0
- data/lib/her/relation.rb +94 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +42 -0
- data/spec/middleware/second_level_parse_json_spec.rb +25 -0
- data/spec/model/hooks_spec.rb +406 -0
- data/spec/model/http_spec.rb +184 -0
- data/spec/model/introspection_spec.rb +59 -0
- data/spec/model/orm_spec.rb +552 -0
- data/spec/model/paths_spec.rb +286 -0
- data/spec/model/relationships_spec.rb +222 -0
- data/spec/model_spec.rb +31 -0
- data/spec/spec_helper.rb +46 -0
- metadata +222 -0
data/Rakefile
ADDED
data/UPGRADE.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Upgrade Her
|
2
|
+
|
3
|
+
Here is a list of backward-incompatible changes that were introduced while Her is pre-1.0. After reaching 1.0, it will follow the [Semantic Versioning](http://semver.org/) system.
|
4
|
+
|
5
|
+
## 0.2.4
|
6
|
+
|
7
|
+
* Her no longer includes default middleware when making HTTP requests. The user has now to define all the needed middleware. Before:
|
8
|
+
|
9
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
10
|
+
connection.insert(0, FaradayMiddle::OAuth)
|
11
|
+
end
|
12
|
+
|
13
|
+
Now:
|
14
|
+
|
15
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
16
|
+
connection.use FaradayMiddle::OAuth
|
17
|
+
connection.use Her::Middleware::FirstLevelParseJSON
|
18
|
+
connection.use Faraday::Request::UrlEncoded
|
19
|
+
connection.use Faraday::Adapter::NetHttp
|
20
|
+
end
|
21
|
+
|
22
|
+
## 0.2
|
23
|
+
|
24
|
+
* The default parser middleware has been replaced to treat first-level JSON data as the resource or collection data. Before it expected this:
|
25
|
+
|
26
|
+
{ "data": { "id": 1, "name": "Foo" }, "errors": [] }
|
27
|
+
|
28
|
+
Now it expects this (the `errors` key is not treated as resource data):
|
29
|
+
|
30
|
+
{ "id": 1, "name": "Foo", "errors": [] }
|
31
|
+
|
32
|
+
If you still want to get the old behavior, you can use `Her::Middleware::SecondLevelParseJSON` instead of `Her::Middleware::FirstLevelParseJSON` in your middleware stack.
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Create custom parser
|
2
|
+
class TwitterParser < Faraday::Response::Middleware
|
3
|
+
def on_complete(env)
|
4
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
5
|
+
errors = [json.delete(:error)]
|
6
|
+
env[:body] = {
|
7
|
+
:data => json,
|
8
|
+
:errors => errors,
|
9
|
+
:metadata => {},
|
10
|
+
}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# See https://dev.twitter.com/apps
|
15
|
+
TWITTER_CREDENTIALS = {
|
16
|
+
:consumer_key => "",
|
17
|
+
:consumer_secret => "",
|
18
|
+
:token => "",
|
19
|
+
:token_secret => ""
|
20
|
+
}
|
21
|
+
|
22
|
+
# Initialize API
|
23
|
+
Her::API.setup :url => "https://api.twitter.com/1/" do |builder|
|
24
|
+
builder.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
|
25
|
+
builder.use Faraday::Request::UrlEncoded
|
26
|
+
builder.use TwitterParser
|
27
|
+
builder.use Faraday::Adapter::NetHttp
|
28
|
+
end
|
29
|
+
|
30
|
+
# Define classes
|
31
|
+
class Tweet
|
32
|
+
include Her::Model
|
33
|
+
|
34
|
+
def self.timeline
|
35
|
+
get "/statuses/home_timeline.json"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.mentions
|
39
|
+
get "/statuses/mentions.json"
|
40
|
+
end
|
41
|
+
|
42
|
+
def username
|
43
|
+
user[:screen_name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
get "/" do
|
48
|
+
@tweets = Tweet.mentions
|
49
|
+
haml :index
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Create custom parser
|
2
|
+
class TwitterSearchParser < Faraday::Response::Middleware
|
3
|
+
METADATA_KEYS = [:completed_in, :max_id, :max_id_str, :next_page, :page, :query, :refresh_url, :results_per_page, :since_id, :since_id_str]
|
4
|
+
|
5
|
+
def on_complete(env)
|
6
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
7
|
+
data = json.delete(:results)
|
8
|
+
errors = [json.delete(:error)].compact
|
9
|
+
env[:body] = {
|
10
|
+
:data => data,
|
11
|
+
:errors => errors,
|
12
|
+
:metadata => json
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class MyCache < Hash
|
18
|
+
def read(key)
|
19
|
+
if cached = self[key]
|
20
|
+
Marshal.load(cached)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(key, data)
|
25
|
+
self[key] = Marshal.dump(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch(key)
|
29
|
+
read(key) || yield.tap { |data| write(key, data) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
$cache = MyCache.new
|
34
|
+
|
35
|
+
# Initialize API
|
36
|
+
Her::API.setup :url => "http://search.twitter.com" do |connection|
|
37
|
+
connection.use Faraday::Request::UrlEncoded
|
38
|
+
connection.use FaradayMiddleware::Caching, $cache
|
39
|
+
connection.use TwitterSearchParser
|
40
|
+
connection.use Faraday::Adapter::NetHttp
|
41
|
+
end
|
42
|
+
|
43
|
+
# Define classes
|
44
|
+
class Tweet
|
45
|
+
include Her::Model
|
46
|
+
|
47
|
+
def self.search(query, attrs={})
|
48
|
+
get("/search.json", attrs.merge(:q => query))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
get "/" do
|
53
|
+
@tweets = Tweet.search("justin bieber", :rpp => 30)
|
54
|
+
haml :index
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'her/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'extended_her'
|
7
|
+
s.version = Her::VERSION
|
8
|
+
s.authors = ['Rémi Prévost', 'Gregory Eremin']
|
9
|
+
s.email = ['remi@exomel.com']
|
10
|
+
s.homepage = 'http://remiprev.github.com/her'
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.summary = 'A simple Representational State Transfer-based Hypertext Transfer Protocol-powered Object Relational Mapper. Her?'
|
13
|
+
s.description = 'Her is an ORM that maps REST resources and collections to Ruby objects'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_development_dependency 'rake', '~> 10.0'
|
21
|
+
s.add_development_dependency 'rspec', '~> 2.12'
|
22
|
+
s.add_development_dependency 'mocha', '~> 0.13'
|
23
|
+
|
24
|
+
s.add_runtime_dependency 'activesupport', '>= 3.0.0'
|
25
|
+
s.add_runtime_dependency 'faraday', '~> 0.8'
|
26
|
+
s.add_runtime_dependency 'multi_json', '~> 1.5'
|
27
|
+
end
|
data/lib/her.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'her/version'
|
2
|
+
|
3
|
+
require 'multi_json'
|
4
|
+
require 'faraday'
|
5
|
+
require 'active_support'
|
6
|
+
require 'active_support/inflector'
|
7
|
+
require 'active_support/core_ext/hash'
|
8
|
+
|
9
|
+
require 'her/exceptions/exception'
|
10
|
+
require 'her/exceptions/record_invalid'
|
11
|
+
require 'her/exceptions/record_not_found'
|
12
|
+
|
13
|
+
require 'her/model'
|
14
|
+
require 'her/relation'
|
15
|
+
require 'her/api'
|
16
|
+
require 'her/middleware'
|
17
|
+
require 'her/errors'
|
18
|
+
require 'her/collection'
|
19
|
+
require 'her/paginated_collection'
|
20
|
+
require 'her/base'
|
21
|
+
|
22
|
+
module Her
|
23
|
+
end
|
data/lib/her/api.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module Her
|
2
|
+
# This class is where all HTTP requests are made. Before using Her, you must configure it
|
3
|
+
# so it knows where to make those requests. In Rails, this is usually done in `config/initializers/her.rb`:
|
4
|
+
class API
|
5
|
+
# @private
|
6
|
+
attr_reader :base_uri, :connection, :options
|
7
|
+
|
8
|
+
# Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
|
9
|
+
def self.setup(attrs={}, &block)
|
10
|
+
@@default_api = new
|
11
|
+
@@default_api.setup(attrs, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
|
15
|
+
# If your application uses only one API, you should use Her::API.setup to configure the default API
|
16
|
+
#
|
17
|
+
# @example Setting up a new API
|
18
|
+
# api = Her::API.new :url => "https://api.example" do |connection|
|
19
|
+
# connection.use Faraday::Request::UrlEncoded
|
20
|
+
# connection.use Her::Middleware::DefaultParseJSON
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class User
|
24
|
+
# uses_api api
|
25
|
+
# end
|
26
|
+
def initialize(*args, &blk)
|
27
|
+
self.setup(*args, &blk)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Setup the API connection.
|
31
|
+
#
|
32
|
+
# @param [Hash] attrs the Faraday options
|
33
|
+
# @option attrs [String] :url The main HTTP API root (eg. `https://api.example.com`)
|
34
|
+
# @option attrs [String] :ssl A hash containing [SSL options](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates)
|
35
|
+
#
|
36
|
+
# @return Faraday::Connection
|
37
|
+
#
|
38
|
+
# @example Setting up the default API connection
|
39
|
+
# Her::API.setup :url => "https://api.example"
|
40
|
+
#
|
41
|
+
# @example A custom middleware added to the default list
|
42
|
+
# class MyAuthentication < Faraday::Middleware
|
43
|
+
# def call(env)
|
44
|
+
# env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
|
45
|
+
# @all.call(env)
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# Her::API.setup :url => "https://api.example.com" do |connection|
|
49
|
+
# connection.use Faraday::Request::UrlEncoded
|
50
|
+
# connection.use Her::Middleware::DefaultParseJSON
|
51
|
+
# connection.use Faraday::Adapter::NetHttp
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# @example A custom parse middleware
|
55
|
+
# class MyCustomParser < Faraday::Response::Middleware
|
56
|
+
# def on_complete(env)
|
57
|
+
# json = JSON.parse(env[:body], :symbolize_names => true)
|
58
|
+
# errors = json.delete(:errors) || {}
|
59
|
+
# metadata = json.delete(:metadata) || []
|
60
|
+
# env[:body] = { :data => json, :errors => errors, :metadata => metadata }
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
# Her::API.setup :url => "https://api.example.com" do |connection|
|
64
|
+
# connection.use Faraday::Request::UrlEncoded
|
65
|
+
# connection.use MyCustomParser
|
66
|
+
# connection.use Faraday::Adapter::NetHttp
|
67
|
+
# end
|
68
|
+
def setup(attrs={}, &blk)
|
69
|
+
attrs[:url] = attrs.delete(:base_uri) if attrs.include?(:base_uri) # Support legacy :base_uri option
|
70
|
+
@base_uri = attrs[:url]
|
71
|
+
@options = attrs
|
72
|
+
@connection = Faraday.new(@options) do |connection|
|
73
|
+
yield connection if block_given?
|
74
|
+
end
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Define a custom parsing procedure. The procedure is passed the response object and is
|
79
|
+
# expected to return a hash with three keys: a main data Hash, an errors Hash
|
80
|
+
# and a metadata Hash.
|
81
|
+
#
|
82
|
+
# @private
|
83
|
+
def request(attrs={})
|
84
|
+
method = attrs.delete(:_method)
|
85
|
+
path = attrs.delete(:_path)
|
86
|
+
headers = attrs.delete(:_headers)
|
87
|
+
attrs.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
|
88
|
+
response = @connection.send method do |request|
|
89
|
+
request.headers.merge!(headers) if headers
|
90
|
+
if method == :get
|
91
|
+
# For GET requests, treat additional parameters as querystring data
|
92
|
+
request.url path, attrs
|
93
|
+
else
|
94
|
+
# For POST, PUT and DELETE requests, treat additional parameters as request body
|
95
|
+
request.url path
|
96
|
+
request.body = attrs
|
97
|
+
end
|
98
|
+
end
|
99
|
+
response.env[:body]
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
# @private
|
104
|
+
def self.default_api(attrs={})
|
105
|
+
defined?(@@default_api) ? @@default_api : nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/her/base.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Her
|
2
|
+
class Base # In case you prefer inheritance over mixins
|
3
|
+
include Model
|
4
|
+
include ActiveModel::Conversion if defined?(ActiveModel)
|
5
|
+
include ActiveModel::AttributeMethods if defined?(ActiveModel)
|
6
|
+
extend ActiveModel::Naming if defined?(ActiveModel)
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def inherited(klass)
|
10
|
+
klass.root_element(klass.name.demodulize.underscore)
|
11
|
+
klass.collection_path(klass.root_element.pluralize)
|
12
|
+
klass.resource_path([klass.collection_path, '/:id'].join)
|
13
|
+
klass.uses_api(Her::API.default_api)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/her/errors.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Her
|
2
|
+
class RecordNotFound < Exception
|
3
|
+
class << self
|
4
|
+
def one(model, id)
|
5
|
+
new("Couldn't find #{model.name} with id=#{id}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def some(model, ids, found, looking_for)
|
9
|
+
super("Couldn't find all #{model.name}s with IDs (#{ids.join(', ')}) (found #{found} results, but was looking for #{looking_for})")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|