hyperresource 0.1.0

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.
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