hyperresource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2fd8d70a1aa3c15bde69a7a26ca532232c1795cd
4
+ data.tar.gz: 87cf162990ed41accbb7fdf7b3bcc2abded565f7
5
+ SHA512:
6
+ metadata.gz: 3b6b9a4e0a3e19f78af2d68ac3b9fe208b1db41b0dc413f393d9a7b20a66ea853f95e2836f9524406573251d42cb5cbaab642b6489c7d7f5a1ee216ffadf426a
7
+ data.tar.gz: 62fc3bf454b4b54427c9d481c4556039119ba48ddcddbda4aee3c14f68f1b2d3288a7ccebbf2ef38b1b3fbe2914ef4b83f06c93a6bc70c082a4875f1985d396d
@@ -0,0 +1,154 @@
1
+ require 'hyper_resource/version'
2
+ require 'hyper_resource/attributes'
3
+ require 'hyper_resource/links'
4
+ require 'hyper_resource/link'
5
+ require 'hyper_resource/objects'
6
+ require 'hyper_resource/response'
7
+
8
+ require 'hyper_resource/modules/utils'
9
+ require 'hyper_resource/modules/http'
10
+ require 'hyper_resource/modules/bless'
11
+
12
+ require 'pp'
13
+
14
+ class HyperResource
15
+ include HyperResource::Modules::Utils
16
+ include HyperResource::Modules::HTTP
17
+ include HyperResource::Modules::Bless
18
+
19
+ private
20
+
21
+ def self._hr_class_attributes
22
+ %w( root auth headers namespace ).map(&:to_sym)
23
+ end
24
+
25
+ def self._hr_attributes
26
+ %w( root href auth headers namespace
27
+ request response response_body
28
+ attributes links objects loaded).map(&:to_sym)
29
+ end
30
+
31
+ public
32
+
33
+ _hr_class_attributes.each {|attr| class_attribute attr}
34
+ _hr_attributes.each {|attr| attr_accessor attr}
35
+
36
+ # :nodoc:
37
+ DEFAULT_HEADERS = {
38
+ 'Accept' => 'application/json'
39
+ }
40
+
41
+ ## Create a new HyperResource, given a hash of options. These options
42
+ ## include:
43
+ ##
44
+ ## [root] The root URL of the resource.
45
+ ##
46
+ ## [auth] Authentication information. Currently only +{basic:
47
+ ## ['key', 'secret']}+ is supported.
48
+ ##
49
+ ## [namespace] Class or class name, into which resources should be
50
+ ## instantiated.
51
+ ##
52
+ ## [headers] Headers to send along with requests for this resource (as
53
+ ## well as its eventual child resources, if any).
54
+ def initialize(opts={})
55
+ if opts.is_a?(HyperResource)
56
+ self.class._hr_attributes.each {|attr| self.send("#{attr}=".to_sym, opts.send(attr))}
57
+ return
58
+ end
59
+
60
+ self.root = opts[:root] || self.class.root
61
+ self.href = opts[:href] || ''
62
+ self.auth = (self.class.auth || {}).merge(opts[:auth] || {})
63
+ self.namespace = opts[:namespace] || self.class.namespace
64
+ self.headers = DEFAULT_HEADERS.merge(self.class.headers || {}).
65
+ merge(opts[:headers] || {})
66
+
67
+ self.attributes = Attributes.new(self)
68
+ self.links = Links.new(self)
69
+ self.objects = Objects.new(self)
70
+ self.loaded = false
71
+ end
72
+
73
+ ## Returns a new HyperResource based on the given HyperResource object.
74
+ def new_from_resource(rsrc); self.class.new_from_resource(rsrc) end
75
+ def self.new_from_resource(rsrc)
76
+ new_rsrc = self.new
77
+ _hr_attributes.each do |attr|
78
+ new_rsrc.send("#{attr}=".to_sym, rsrc.send(attr))
79
+ end
80
+ new_rsrc
81
+ end
82
+
83
+ ## Returns a new HyperResource based on the given HAL document.
84
+ def new_from_hal(obj)
85
+ rsrc = self.class.new(:root => self.root,
86
+ :auth => self.auth,
87
+ :headers => self.headers,
88
+ :namespace => self.namespace,
89
+ :href => obj['_links']['self']['href'])
90
+ rsrc.response_body = Response[obj]
91
+ rsrc.init_from_response_body!
92
+ rsrc
93
+ end
94
+
95
+ ## Returns a new HyperResource based on the given link href.
96
+ def new_from_link(href)
97
+ rsrc = self.class.new(:root => self.root,
98
+ :auth => self.auth,
99
+ :headers => self.headers,
100
+ :namespace => self.namespace,
101
+ :href => href)
102
+ end
103
+
104
+ ## Populates +attributes+, +links+, and +objects+ from the contents of
105
+ ## +response+. Sets +loaded = true+.
106
+ def init_from_response_body!
107
+ return unless self.response_body
108
+ self.objects. init_from_hal(self.response_body);
109
+ self.links. init_from_hal(self.response_body);
110
+ self.attributes.init_from_hal(self.response_body);
111
+ self.loaded = true
112
+ self
113
+ end
114
+
115
+ ## Returns the first object in the first collection of objects embedded
116
+ ## in this resource. Equivalent to +self.objects.first+.
117
+ def first; self.objects.first end
118
+
119
+ ## Returns the *i*th object in the first collection of objects embedded
120
+ ## in this resource. Equivalent to +self.objects[i]+.
121
+ def [](i); self.objects[i] end
122
+
123
+
124
+ ## method_missing will load this resource if not yet loaded, then
125
+ ## attempt to delegate to +attributes+, then +objects+,
126
+ ## then +links+. When it finds a match, it will define a method class-wide
127
+ ## if self.class != HyperResource, instance-wide otherwise.
128
+ def method_missing(method, *args)
129
+ self.get unless self.loaded
130
+ [:attributes, :objects, :links].each do |field|
131
+ if self.send(field).respond_to?(method)
132
+ if self.class == HyperResource
133
+ define_singleton_method(method) do |*args|
134
+ self.send(field).send(method, *args)
135
+ end
136
+ else
137
+ self.class.send(:define_method, method) do |*args|
138
+ self.send(field).send(method, *args)
139
+ end
140
+ end
141
+ return self.send(field).send(method, *args)
142
+ end
143
+ end
144
+ super
145
+ end
146
+
147
+
148
+ def inspect # :nodoc:
149
+ "#<#{self.class}:0x#{"%x" % self.object_id} @root=#{self.root.inspect} "+
150
+ "@href=#{self.href.inspect} @loaded=#{self.loaded} "+
151
+ "@namespace=#{self.namespace.inspect} ...>"
152
+ end
153
+
154
+ end
@@ -0,0 +1,24 @@
1
+ class HyperResource::Attributes < Hash
2
+ attr_accessor :parent_resource
3
+ def initialize(resource=nil)
4
+ self.parent_resource = resource || HyperResource.new
5
+ end
6
+ # Initialize attributes from a HAL response.
7
+ def init_from_hal(hal_resp)
8
+ (hal_resp.keys - ['_links', '_embedded']).map(&:to_s).each do |attr|
9
+ self[attr] = hal_resp[attr]
10
+
11
+ unless self.respond_to?(attr.to_sym)
12
+ define_singleton_method(attr.to_sym) { self[attr] }
13
+ define_singleton_method("#{attr}=".to_sym) {|v| self[attr]=v}
14
+ end
15
+
16
+ unless self.parent_resource.respond_to?(attr.to_sym)
17
+ self.parent_resource.define_singleton_method(attr.to_sym) {self[attr]}
18
+ self.parent_resource.define_singleton_method("#{attr}=".to_sym) do |v|
19
+ self[attr] = v
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,51 @@
1
+ require 'uri_template'
2
+
3
+ class HyperResource::Link
4
+ attr_accessor :base_href,
5
+ :templated,
6
+ :params,
7
+ :parent_resource
8
+
9
+ def templated?; templated end
10
+
11
+ def initialize(resource=nil, link_spec={})
12
+ self.parent_resource = resource || HyperResource.new
13
+ self.base_href = link_spec['href']
14
+ self.templated = !!link_spec['templated']
15
+ self.params = link_spec['params'] || {}
16
+ end
17
+
18
+ ## Returns this link's href, applying any URI template params.
19
+ def href
20
+ if self.templated?
21
+ URITemplate.new(self.base_href).expand(params)
22
+ else
23
+ self.base_href
24
+ end
25
+ end
26
+
27
+ ## Returns a new scope with the given params; that is, returns a copy of
28
+ ## itself with the given params applied.
29
+ def where(params)
30
+ self.class.new(self.parent_resource,
31
+ 'href' => self.base_href,
32
+ 'templated' => self.templated,
33
+ 'params' => self.params.merge(params))
34
+ end
35
+
36
+ ## Returns a HyperResource representing this link
37
+ def resource
38
+ parent_resource.new_from_link(self.href)
39
+ end
40
+
41
+ ## Returns a HyperResource representing this link, and fetches it.
42
+ def get
43
+ self.resource.get
44
+ end
45
+
46
+ ## If we were called with a method we don't know, load this resource
47
+ ## and pass the message along. This achieves implicit loading.
48
+ def method_missing(method, *args)
49
+ self.get.send(method, *args)
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ class HyperResource
2
+ class Links < Hash
3
+ attr_accessor :resource
4
+
5
+ def initialize(resource=nil)
6
+ self.resource = resource || HyperResource.new
7
+ end
8
+
9
+ # Initialize links from a HAL response.
10
+ def init_from_hal(hal_resp)
11
+ return unless hal_resp['_links']
12
+ hal_resp['_links'].each do |rel, link_spec|
13
+ self[rel] = HyperResource::Link.new(resource, link_spec)
14
+ unless self.respond_to?(rel.to_sym)
15
+ define_singleton_method(rel.to_sym) { self[rel] }
16
+ end
17
+ unless self.resource.respond_to?(rel.to_sym)
18
+ self.resource.define_singleton_method(rel.to_sym) {self.links[rel]}
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+
26
+
@@ -0,0 +1,57 @@
1
+ module HyperResource::Modules::Bless
2
+
3
+ ## Returns this resource as an instance of +self.resource_class+.
4
+ ## The returned object will share structure with the source object;
5
+ ## beware.
6
+ def blessed
7
+ return self unless self.namespace
8
+ self.resource_class.new(self)
9
+ end
10
+
11
+ ## Returns the class into which this resource should be cast.
12
+ ## If the object is not loaded yet, or if +self.namespace+ is
13
+ ## not set, returns +self.class+.
14
+ ##
15
+ ## Otherwise, +resource_class+ looks at the returned content-type, and
16
+ ## attempts to match a 'type=...' modifier. Given a namespace of
17
+ ## +FooAPI+ and a response content-type of
18
+ ## "application/vnd.foocorp.fooapi.v1+json;type=User", this should
19
+ ## return +FooAPI::User+ (even if +FooAPI::User+ hadn't existed yet).
20
+ def resource_class
21
+ return self.class unless self.namespace
22
+ return self.class unless type_name = self.data_type_name
23
+ class_name = "#{self.namespace}::#{type_name}".
24
+ gsub(/[^_0-9A-Za-z:]/, '')
25
+
26
+ ## Return data type class if it exists
27
+ klass = eval(class_name) rescue :sorry_dude
28
+ return klass if klass.is_a?(Class)
29
+
30
+ ## Data type class didn't exist -- create namespace (if necessary),
31
+ ## then the data type class
32
+ if self.namespace != ''
33
+ nsc = eval(self.namespace) rescue :bzzzzzt
34
+ unless nsc.is_a?(Class)
35
+ Object.module_eval "class #{self.namespace} < #{self.class}; end"
36
+ end
37
+ end
38
+ Object.module_eval "class #{class_name} < #{self.namespace}; end"
39
+ eval(class_name)
40
+ end
41
+
42
+ ## Inspects the response, and returns a string describing this
43
+ ## resource's data type.
44
+ ##
45
+ ## By default, this method looks for a +type=...+ modifier in the
46
+ ## response's +Content-type+. Override this method in a
47
+ ## HyperResource subclass in order to implement different data type
48
+ ## detection.
49
+ def data_type_name
50
+ return nil unless self.response
51
+ return nil unless content_type = self.response['content-type']
52
+ return nil unless m=content_type.match(/;\s* type=(?<type> [0-9A-Za-z:]+)/x)
53
+ m[:type][0].upcase + m[:type][1..-1]
54
+ end
55
+
56
+
57
+ end
@@ -0,0 +1,39 @@
1
+ require 'faraday'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+
6
+ module HyperResource::Modules::HTTP
7
+
8
+ ## Loads and returns the resource pointed to by +href+. The returned
9
+ ## resource will be blessed into its "proper" class, if
10
+ ## +self.class.namespace != nil+.
11
+ def get
12
+ self.response = faraday_connection.get(self.href || '')
13
+ finish_up
14
+ end
15
+
16
+ ## Returns a per-thread Faraday connection for this object.
17
+ def faraday_connection(url=nil)
18
+ url ||= self.root
19
+ key = "faraday_connection_#{url}"
20
+ return Thread.current[key] if Thread.current[key]
21
+
22
+ fc = Faraday.new(:url => url)
23
+ fc.headers.merge!('User-Agent' => "HyperResource #{HyperResource::VERSION}")
24
+ fc.headers.merge!(self.headers || {})
25
+ if ba=self.auth[:basic]
26
+ fc.basic_auth(*ba)
27
+ end
28
+ Thread.current[key] = fc
29
+ end
30
+
31
+ private
32
+
33
+ def finish_up
34
+ self.response_body = JSON.parse(self.response.body)
35
+ self.init_from_response_body!
36
+ self.blessed
37
+ end
38
+
39
+ end
@@ -0,0 +1,29 @@
1
+ module HyperResource::Modules
2
+ module Utils
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ ## Inheritable class attribute, kinda like in Rails.
11
+ def class_attribute(*names)
12
+ names.map(&:to_sym).each do |name|
13
+ instance_eval <<-EOT
14
+ def #{name}=(val)
15
+ @#{name} = val
16
+ end
17
+ def #{name}
18
+ return @#{name} if defined?(@#{name})
19
+ return superclass.#{name} if superclass.respond_to?(:#{name})
20
+ nil
21
+ end
22
+ EOT
23
+ end
24
+ end
25
+
26
+ end # module ClassMethods
27
+
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ class HyperResource::Objects < Hash
2
+ attr_accessor :parent_resource
3
+ def initialize(parent_resource=nil)
4
+ self.parent_resource = parent_resource || HyperResource.new
5
+ end
6
+ def init_from_hal(hal_resp)
7
+ return unless hal_resp['_embedded']
8
+ hal_resp['_embedded'].each do |name, collection|
9
+ self[name] = collection.map do |obj|
10
+ self.parent_resource.new_from_hal(obj)
11
+ end
12
+ unless self.respond_to?(name.to_sym)
13
+ define_singleton_method(name.to_sym) { self[name] }
14
+ end
15
+ unless self.parent_resource.respond_to?(name.to_sym)
16
+ self.parent_resource.define_singleton_method(name.to_sym) {self[name]}
17
+ end
18
+ end
19
+ end
20
+
21
+ ## Returns the first item in the first collection in +self+.
22
+ alias_method :first_orig, :first
23
+ def first
24
+ self.first_orig[1][0]
25
+ end
26
+
27
+ ## Returns the ith item in the first collection in +self+.
28
+ def [](i)
29
+ self.first_orig[1][i]
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ class HyperResource::Response < Hash
2
+ end
@@ -0,0 +1,4 @@
1
+ class HyperResource
2
+ VERSION = '0.1.0'
3
+ VERSION_DATE = '2013-04-07'
4
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperresource
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pete Gamache
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: uri_template
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.8.6
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 10.0.4
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 10.0.4
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 4.7.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: 4.7.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.13.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.13.3
83
+ description: |2
84
+ HyperResource is a hypermedia client library for Ruby. Its goals are to
85
+ interface directly with well-behaved hypermedia APIs, to allow the data
86
+ returned from these APIs to optionally be extended by client-side code,
87
+ and to present a modern replacement for ActiveResource.
88
+ email: pete@gamache.org
89
+ executables: []
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - lib/hyper_resource/attributes.rb
94
+ - lib/hyper_resource/link.rb
95
+ - lib/hyper_resource/links.rb
96
+ - lib/hyper_resource/modules/bless.rb
97
+ - lib/hyper_resource/modules/http.rb
98
+ - lib/hyper_resource/modules/utils.rb
99
+ - lib/hyper_resource/objects.rb
100
+ - lib/hyper_resource/response.rb
101
+ - lib/hyper_resource/version.rb
102
+ - lib/hyper_resource.rb
103
+ homepage: https://github.com/gamache/hyperresource
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - '>='
114
+ - !ruby/object:Gem::Version
115
+ version: 1.8.7
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.0.2
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Extensible hypermedia client for Ruby
127
+ test_files: []
128
+ has_rdoc: true