halibut 0.1.0 → 0.2.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.
- data/.gitignore +4 -1
- data/.gitmodules +3 -0
- data/.travis.yml +4 -4
- data/Gemfile +2 -2
- data/Gemfile.travis +11 -0
- data/README.md +83 -34
- data/Rakefile +2 -2
- data/TODO +2 -0
- data/halibut.gemspec +16 -11
- data/lib/halibut.rb +15 -3
- data/lib/halibut/adapter/json.rb +72 -0
- data/lib/halibut/adapter/xml.rb +88 -0
- data/lib/halibut/builder.rb +75 -0
- data/lib/halibut/hal/link.rb +81 -0
- data/lib/halibut/hal/resource.rb +74 -0
- data/lib/halibut/link_relation.rb +29 -0
- data/lib/halibut/relation_map.rb +24 -23
- data/lib/halibut/version.rb +1 -2
- data/spec/adapter/json_spec.rb +72 -0
- data/spec/adapter/xml_spec.rb +50 -0
- data/spec/builder_spec.rb +185 -0
- data/spec/fixtures/serialize.json +1 -1
- data/spec/link_relation_spec.rb +43 -0
- data/spec/link_spec.rb +13 -11
- data/spec/relation_map_spec.rb +7 -6
- data/spec/resource_spec.rb +23 -53
- data/spec/spec_helper.rb +4 -2
- metadata +78 -15
- data/lib/halibut/link.rb +0 -56
- data/lib/halibut/resource.rb +0 -94
data/.gitignore
CHANGED
data/.gitmodules
ADDED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.travis
ADDED
data/README.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
# Halibut
|
2
2
|
|
3
|
-
|
3
|
+
Halibut is a tiny gem that makes it easier to deal with the [HAL](http://stateless.co/hal_specification.html) format.
|
4
|
+
|
5
|
+
In providing tools to build, (de)serialize, and manipulate HAL resources,
|
6
|
+
Halibut has the following goals:
|
7
|
+
|
8
|
+
- Provide Ruby abstractions
|
9
|
+
- Clean, small API
|
10
|
+
- Easily composable, for use in other libraries
|
4
11
|
|
5
12
|
## Installation
|
6
13
|
|
@@ -18,56 +25,98 @@ Or install it yourself as:
|
|
18
25
|
|
19
26
|
## Usage
|
20
27
|
|
21
|
-
|
28
|
+
There are three ways to get a resource with halibut: manual, Builder, and JSON.
|
22
29
|
|
23
|
-
|
30
|
+
### Manual
|
24
31
|
|
25
32
|
```ruby
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
Halibut::HAL
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
+
require 'halibut'
|
34
|
+
|
35
|
+
# manually creating a resource
|
36
|
+
order = Halibut::HAL::Resource.new "/orders/123"
|
37
|
+
order.set_property "total", 30.00
|
38
|
+
order.set_property "currency", "USD"
|
39
|
+
order.set_property "status", "shipped"
|
40
|
+
|
41
|
+
resource = Halibut::HAL::Resource.new "/orders"
|
42
|
+
resource.add_link "find", "/orders{?id}", templated: true
|
43
|
+
resource.add_link "next", "/orders/1", "name" => 'hotdog'
|
44
|
+
resource.add_link "next", "/orders/9"
|
45
|
+
resource.set_property "currentlyProcessing", 14
|
46
|
+
resource.set_property "shippedToday", 20
|
47
|
+
resource.embed_resource "orders", order
|
48
|
+
```
|
33
49
|
|
34
|
-
Halibut::
|
35
|
-
|
50
|
+
### Halibut::Builder
|
51
|
+
```ruby
|
52
|
+
require 'halibut/builder'
|
36
53
|
|
37
|
-
Halibut::
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
54
|
+
builder = Halibut::Builder.new '/orders' do
|
55
|
+
property 'currentlyProcessing', 14
|
56
|
+
property 'shippedToday', 20
|
57
|
+
|
58
|
+
namespace 'th', 'http://things-db.com/{rel}'
|
59
|
+
|
60
|
+
link 'find', '/orders{?id}', templated: true
|
61
|
+
link 'next', '/orders/1', name: 'hotdog'
|
62
|
+
link 'next', '/orders/9'
|
63
|
+
|
64
|
+
link 'th:manufacturer', '/manufacturer/1'
|
65
|
+
link 'th:manufacturer', '/manufacturer/2'
|
66
|
+
link 'th:manufacturer', '/manufacturer/3'
|
67
|
+
end
|
43
68
|
|
44
|
-
|
45
|
-
it.attr "some_attribute", "The Value of the Attribute"
|
46
|
-
it.attr "another_attribute", "The Value of Another Attribute"
|
69
|
+
# alternatively
|
47
70
|
|
48
|
-
|
71
|
+
builder = Halibut::Builder.new '/orders' do
|
72
|
+
property 'currentlyProcessing', 14
|
73
|
+
property 'shippedToday', 20
|
74
|
+
|
75
|
+
namespace 'th', 'http://things-db.com/{rel}'
|
49
76
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
r.link "search", "/href/etc?search={term}", :templated => true, :title => "Embedded Resource"
|
77
|
+
link 'find', '/orderes{?id}', templated: true
|
78
|
+
relation 'next' do
|
79
|
+
link '/orders/1', name: 'hotdog'
|
80
|
+
link '/orders/9'
|
55
81
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
r.link "search", "/href/more?search={term}", :templated => true, :title => "Embedded Resource"
|
82
|
+
relation 'th:manufacturer' do
|
83
|
+
link '/manufacturers/1'
|
84
|
+
link '/manufacturers/2'
|
85
|
+
link '/manufacturers/3'
|
61
86
|
end
|
62
87
|
end
|
63
|
-
hal.to_xml
|
64
88
|
|
65
|
-
|
89
|
+
resource = builder.resource
|
90
|
+
```
|
91
|
+
|
92
|
+
### JSON
|
93
|
+
```ruby
|
94
|
+
require 'halibut/adapter/json'
|
95
|
+
|
96
|
+
# converting to JSON
|
97
|
+
Halibut::Adapter::JSON.dump resource
|
98
|
+
|
99
|
+
# creating a resource from JSON
|
100
|
+
resource = Halibut::Adapter::JSON.load 'resource.json'
|
101
|
+
```
|
102
|
+
|
103
|
+
### XML
|
104
|
+
```ruby
|
105
|
+
require 'halibut/adapter/xml'
|
106
|
+
|
107
|
+
# converting to XML
|
108
|
+
# Coming soon…
|
109
|
+
|
110
|
+
# creating a resource from XML
|
111
|
+
resource = Halibut::Adapter::XML.load 'resource.xml'
|
66
112
|
```
|
67
113
|
|
68
114
|
## Contributing
|
69
115
|
|
70
116
|
1. Fork it
|
117
|
+
1. `git submodule update --init`
|
118
|
+
2. `bundle install`, optionally pass `--without-development` and use your
|
119
|
+
own tools
|
71
120
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
72
121
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
73
122
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/Rakefile
CHANGED
data/TODO
ADDED
data/halibut.gemspec
CHANGED
@@ -14,22 +14,27 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.name = "halibut"
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Halibut::VERSION
|
17
|
-
|
17
|
+
|
18
|
+
gem.required_ruby_version = '~> 1.9.3'
|
19
|
+
|
18
20
|
gem.add_dependency "multi_json"
|
19
21
|
gem.add_dependency "nokogiri"
|
20
|
-
gem.add_dependency "
|
21
|
-
|
22
|
-
gem.add_development_dependency "
|
23
|
-
|
24
|
-
gem.add_development_dependency "pry"
|
25
|
-
gem.add_development_dependency "pry-
|
26
|
-
gem.add_development_dependency "pry-
|
27
|
-
|
28
|
-
|
29
|
-
gem.add_development_dependency "
|
22
|
+
gem.add_dependency "addressable"
|
23
|
+
|
24
|
+
gem.add_development_dependency "minitest", ">= 4.1"
|
25
|
+
|
26
|
+
gem.add_development_dependency "pry", ">= 0.9.10"
|
27
|
+
gem.add_development_dependency "pry-doc"
|
28
|
+
gem.add_development_dependency "pry-stack_explorer"
|
29
|
+
gem.add_development_dependency "pry-coolline", "0.1.5"
|
30
|
+
gem.add_development_dependency "pry-rescue", "0.13"
|
31
|
+
gem.add_development_dependency "hash-differ"
|
32
|
+
|
30
33
|
gem.add_development_dependency "guard"
|
31
34
|
gem.add_development_dependency "guard-bundler"
|
32
35
|
gem.add_development_dependency "guard-minitest"
|
33
36
|
|
37
|
+
gem.add_development_dependency "rb-fsevent"
|
34
38
|
gem.add_development_dependency "terminal-notifier-guard"
|
39
|
+
|
35
40
|
end
|
data/lib/halibut.rb
CHANGED
@@ -2,8 +2,20 @@ require "halibut/version"
|
|
2
2
|
|
3
3
|
# Halibut is the main namespace
|
4
4
|
module Halibut
|
5
|
+
autoload :Builder, 'halibut/builder'
|
6
|
+
|
7
|
+
autoload :RelationMap, 'halibut/relation_map'
|
8
|
+
end
|
9
|
+
|
10
|
+
# The Adapter namespace contains classes that aid in the
|
11
|
+
# mapping of HAL Resources into a specific format.
|
12
|
+
module Halibut::Adapter
|
13
|
+
autoload :JSON, 'halibut/adapter/json'
|
14
|
+
autoload :XML, 'halibut/adapter/xml'
|
5
15
|
end
|
6
16
|
|
7
|
-
|
8
|
-
|
9
|
-
|
17
|
+
# Halibut::HAL contains the domain objects that reflect the HAL specs.
|
18
|
+
module Halibut::HAL
|
19
|
+
autoload :Link, 'halibut/hal/link'
|
20
|
+
autoload :Resource, 'halibut/hal/resource'
|
21
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Halibut::Adapter
|
4
|
+
|
5
|
+
module JSON
|
6
|
+
def self.extended(base)
|
7
|
+
base.extend InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(json)
|
11
|
+
ResourceExtractor.new(json).resource
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.dump(resource)
|
15
|
+
MultiJson.dump resource.to_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
module InstanceMethods
|
20
|
+
def to_json
|
21
|
+
MultiJson.dump self.to_hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ResourceExtractor
|
26
|
+
def initialize(json)
|
27
|
+
@halibut = Halibut::HAL::Resource.new
|
28
|
+
json = MultiJson.load(json)
|
29
|
+
|
30
|
+
links = json.delete '_links'
|
31
|
+
resources = json.delete '_embedded'
|
32
|
+
properties = json
|
33
|
+
|
34
|
+
properties and extract_properties properties
|
35
|
+
links and extract_links links
|
36
|
+
resources and extract_resources resources
|
37
|
+
end
|
38
|
+
|
39
|
+
def resource
|
40
|
+
@halibut
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def extract_properties(properties)
|
45
|
+
properties.each_pair do |property, value|
|
46
|
+
@halibut.set_property(property, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def extract_links(links)
|
51
|
+
links.each do |relation,values|
|
52
|
+
links = ([] << values).flatten
|
53
|
+
|
54
|
+
links.each do |attrs|
|
55
|
+
href = attrs.delete 'href'
|
56
|
+
@halibut.add_link(relation, href, attrs)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_resources(resources)
|
62
|
+
resources.each do |relation,values|
|
63
|
+
embeds = ([] << values).flatten
|
64
|
+
|
65
|
+
embeds.map {|embed| MultiJson.dump embed }
|
66
|
+
.map {|embed| Halibut::Adapter::JSON.load embed }
|
67
|
+
.each {|embed| @halibut.embed_resource(relation, embed) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Halibut::Adapter
|
4
|
+
|
5
|
+
module XML
|
6
|
+
def self.extended(base)
|
7
|
+
base.extend InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(xml)
|
11
|
+
ResourceExtractor.new(xml).resource
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.dump(resource)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
module InstanceMethods
|
19
|
+
def to_xml
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ResourceExtractor
|
24
|
+
def initialize(xml)
|
25
|
+
xml = Nokogiri::XML(xml)
|
26
|
+
|
27
|
+
@document = xml.root
|
28
|
+
@halibut = Halibut::HAL::Resource.new extract_self_link
|
29
|
+
|
30
|
+
# binding.pry
|
31
|
+
|
32
|
+
extract_curie
|
33
|
+
extract_properties
|
34
|
+
extract_links
|
35
|
+
extract_resources
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource
|
39
|
+
@halibut
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def extract_self_link
|
44
|
+
@document.attr 'href'
|
45
|
+
end
|
46
|
+
|
47
|
+
def extract_curie
|
48
|
+
@document.namespace_scopes
|
49
|
+
.reject {|ns| ns.prefix.eql? 'xsi' }
|
50
|
+
.each do |ns|
|
51
|
+
@halibut.add_link 'curie', ns.href, name: ns.prefix
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def extract_properties
|
56
|
+
properties = @document.xpath '/resource/*[not(self::link) and not(self::resource)]'
|
57
|
+
|
58
|
+
properties.each do |property|
|
59
|
+
@halibut.set_property property.name, property.content
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def extract_links
|
64
|
+
links = @document.xpath('/resource/link')
|
65
|
+
|
66
|
+
links.each do |link|
|
67
|
+
@halibut.add_link link.attribute('rel').value,
|
68
|
+
link.attribute('href').value,
|
69
|
+
extract_link_options(link)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# In case there are no options on the link, it returns an empty hash
|
74
|
+
def extract_link_options(link)
|
75
|
+
link.attributes.reject {|k,v| k=='rel' || k=='href' }
|
76
|
+
.map {|k,v| { k => v.value } }
|
77
|
+
.reduce(:merge) || {}
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_resources
|
81
|
+
@document.xpath('/resource/resource')
|
82
|
+
.map {|r| [] << r['rel'] << ResourceExtractor.new(r.to_xml).resource }
|
83
|
+
.each {|rel,res| @halibut.embed_resource rel, res }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'halibut/hal/resource'
|
2
|
+
|
3
|
+
module Halibut
|
4
|
+
|
5
|
+
# Builder provides a very thin wrapper around creating a HAL resource.
|
6
|
+
class Builder
|
7
|
+
attr_accessor :resource
|
8
|
+
|
9
|
+
def initialize(href=nil, &blk)
|
10
|
+
@resource = Halibut::HAL::Resource.new href
|
11
|
+
|
12
|
+
RootContext.new(@resource, &blk)
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to?(meth, *args)
|
16
|
+
RootContext.new(@resource).respond_to? meth
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing meth, *args
|
20
|
+
RootContext.new(@resource).send meth, *args
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
class RootContext
|
25
|
+
extend Forwardable
|
26
|
+
|
27
|
+
def_delegator :@resource, :set_property, :property
|
28
|
+
def_delegator :@resource, :add_link, :link
|
29
|
+
|
30
|
+
def initialize(resource, &blk)
|
31
|
+
@resource = resource
|
32
|
+
|
33
|
+
instance_eval(&blk) if block_given?
|
34
|
+
end
|
35
|
+
|
36
|
+
def namespace(name, href)
|
37
|
+
@resource.add_link("curie", href, name: name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def resource(rel, href=nil, &blk)
|
41
|
+
embedded = Halibut::Builder.new(href, &blk)
|
42
|
+
|
43
|
+
@resource.embed_resource(rel, embedded.resource)
|
44
|
+
end
|
45
|
+
|
46
|
+
def relation(rel, &blk)
|
47
|
+
RelationContext.new(@resource, rel, &blk)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
class RelationContext
|
53
|
+
|
54
|
+
def initialize(resource, rel, &blk)
|
55
|
+
@resource = resource
|
56
|
+
@rel = rel
|
57
|
+
|
58
|
+
instance_eval(&blk) if block_given?
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def link(href, opts={})
|
63
|
+
@resource.add_link(@rel, href, opts)
|
64
|
+
end
|
65
|
+
|
66
|
+
def resource(href=nil, &blk)
|
67
|
+
embedded = Halibut::Builder.new(href, &blk)
|
68
|
+
|
69
|
+
@resource.embed_resource(@rel, embedded.resource)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|