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.
Files changed (66) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CONTRIBUTING.md +26 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +7 -0
  7. data/README.md +723 -0
  8. data/Rakefile +11 -0
  9. data/UPGRADE.md +32 -0
  10. data/examples/twitter-oauth/Gemfile +13 -0
  11. data/examples/twitter-oauth/app.rb +50 -0
  12. data/examples/twitter-oauth/config.ru +5 -0
  13. data/examples/twitter-oauth/views/index.haml +9 -0
  14. data/examples/twitter-search/Gemfile +12 -0
  15. data/examples/twitter-search/app.rb +55 -0
  16. data/examples/twitter-search/config.ru +5 -0
  17. data/examples/twitter-search/views/index.haml +9 -0
  18. data/extended_her.gemspec +27 -0
  19. data/lib/her.rb +23 -0
  20. data/lib/her/api.rb +108 -0
  21. data/lib/her/base.rb +17 -0
  22. data/lib/her/collection.rb +12 -0
  23. data/lib/her/errors.rb +5 -0
  24. data/lib/her/exceptions/exception.rb +4 -0
  25. data/lib/her/exceptions/record_invalid.rb +8 -0
  26. data/lib/her/exceptions/record_not_found.rb +13 -0
  27. data/lib/her/middleware.rb +9 -0
  28. data/lib/her/middleware/accept_json.rb +15 -0
  29. data/lib/her/middleware/first_level_parse_json.rb +34 -0
  30. data/lib/her/middleware/second_level_parse_json.rb +28 -0
  31. data/lib/her/model.rb +69 -0
  32. data/lib/her/model/base.rb +7 -0
  33. data/lib/her/model/hooks.rb +114 -0
  34. data/lib/her/model/http.rb +284 -0
  35. data/lib/her/model/introspection.rb +57 -0
  36. data/lib/her/model/orm.rb +191 -0
  37. data/lib/her/model/orm/comparison_methods.rb +20 -0
  38. data/lib/her/model/orm/create_methods.rb +29 -0
  39. data/lib/her/model/orm/destroy_methods.rb +53 -0
  40. data/lib/her/model/orm/error_methods.rb +19 -0
  41. data/lib/her/model/orm/fields_definition.rb +15 -0
  42. data/lib/her/model/orm/find_methods.rb +46 -0
  43. data/lib/her/model/orm/persistance_methods.rb +22 -0
  44. data/lib/her/model/orm/relation_mapper.rb +21 -0
  45. data/lib/her/model/orm/save_methods.rb +58 -0
  46. data/lib/her/model/orm/serialization_methods.rb +28 -0
  47. data/lib/her/model/orm/update_methods.rb +31 -0
  48. data/lib/her/model/paths.rb +82 -0
  49. data/lib/her/model/relationships.rb +191 -0
  50. data/lib/her/paginated_collection.rb +20 -0
  51. data/lib/her/relation.rb +94 -0
  52. data/lib/her/version.rb +3 -0
  53. data/spec/api_spec.rb +131 -0
  54. data/spec/collection_spec.rb +26 -0
  55. data/spec/middleware/accept_json_spec.rb +10 -0
  56. data/spec/middleware/first_level_parse_json_spec.rb +42 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +25 -0
  58. data/spec/model/hooks_spec.rb +406 -0
  59. data/spec/model/http_spec.rb +184 -0
  60. data/spec/model/introspection_spec.rb +59 -0
  61. data/spec/model/orm_spec.rb +552 -0
  62. data/spec/model/paths_spec.rb +286 -0
  63. data/spec/model/relationships_spec.rb +222 -0
  64. data/spec/model_spec.rb +31 -0
  65. data/spec/spec_helper.rb +46 -0
  66. metadata +222 -0
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ require "rake"
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new(:spec) do |task|
10
+ task.pattern = "spec/**/*_spec.rb"
11
+ end
@@ -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,13 @@
1
+ source :rubygems
2
+
3
+ gem "sinatra"
4
+ gem "haml"
5
+ gem "thin", :require => false
6
+ gem "faraday_middleware"
7
+ gem "simple_oauth"
8
+
9
+ gem "her", :path => File.join(File.dirname(__FILE__),"../..")
10
+
11
+ group :development do
12
+ gem "shotgun"
13
+ end
@@ -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,5 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "./app"
5
+ run Sinatra::Application
@@ -0,0 +1,9 @@
1
+ !!!
2
+ %html
3
+ %body
4
+ %ul
5
+ - @tweets.each do |tweet|
6
+ %li{ :style => "margin: 0 0 10px" }
7
+ %span= tweet.text
8
+ %br
9
+ %strong= tweet.username
@@ -0,0 +1,12 @@
1
+ source :rubygems
2
+
3
+ gem "sinatra"
4
+ gem "haml"
5
+ gem "thin", :require => false
6
+ gem "faraday_middleware"
7
+
8
+ gem "her", :path => File.join(File.dirname(__FILE__),"../..")
9
+
10
+ group :development do
11
+ gem "shotgun"
12
+ 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,5 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "./app"
5
+ run Sinatra::Application
@@ -0,0 +1,9 @@
1
+ !!!
2
+ %html
3
+ %body
4
+ %ul
5
+ - @tweets.each do |tweet|
6
+ %li{ :style => "margin: 0 0 10px" }
7
+ %span= tweet.text
8
+ %br
9
+ %strong= tweet.from_user
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,12 @@
1
+ module Her
2
+ class Collection < ::Array
3
+ attr_reader :metadata, :errors
4
+
5
+ # @private
6
+ def initialize(items=[], metadata={}, errors={})
7
+ super(items)
8
+ @metadata = metadata
9
+ @errors = errors
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module Her
2
+ module Errors
3
+ class PathError < StandardError; end;
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Her
2
+ class Exception < ::Exception
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ module Her
2
+ class RecordInvalid < Exception
3
+ def initialize(errors)
4
+ message = errors.map{ |field, message| [field, message].join(' ') }.join(', ')
5
+ super(message)
6
+ end
7
+ end
8
+ end
@@ -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