yaks 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/.gitignore +1 -0
- data/.travis.yml +23 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +32 -6
- data/README.md +182 -80
- data/examples/hal01.rb +126 -0
- data/examples/jsonapi01.rb +68 -0
- data/examples/jsonapi02.rb +62 -0
- data/examples/jsonapi03.rb +86 -0
- data/lib/yaks.rb +55 -25
- data/lib/yaks/collection_mapper.rb +33 -0
- data/lib/yaks/collection_resource.rb +65 -0
- data/lib/yaks/default_policy.rb +13 -0
- data/lib/yaks/hal_serializer.rb +59 -0
- data/lib/yaks/json_api_serializer.rb +59 -0
- data/lib/yaks/link_lookup.rb +23 -0
- data/lib/yaks/mapper.rb +59 -0
- data/lib/yaks/mapper/association.rb +43 -0
- data/lib/yaks/mapper/class_methods.rb +36 -0
- data/lib/yaks/mapper/config.rb +79 -0
- data/lib/yaks/mapper/has_many.rb +15 -0
- data/lib/yaks/mapper/has_one.rb +10 -0
- data/lib/yaks/mapper/link.rb +74 -0
- data/lib/yaks/mapper/lookup.rb +19 -0
- data/lib/yaks/mapper/map_links.rb +17 -0
- data/lib/yaks/null_resource.rb +28 -0
- data/lib/yaks/primitivize.rb +34 -15
- data/lib/yaks/profile_registry.rb +60 -0
- data/lib/yaks/rel_registry.rb +20 -0
- data/lib/yaks/resource.rb +28 -0
- data/lib/yaks/resource/link.rb +21 -0
- data/lib/yaks/serializer.rb +11 -53
- data/lib/yaks/shared_options.rb +15 -0
- data/lib/yaks/util.rb +76 -5
- data/lib/yaks/version.rb +1 -1
- data/spec/integration/map_to_resource_spec.rb +30 -0
- data/spec/json/hal_plant_collection.json +34 -0
- data/spec/spec_helper.rb +10 -1
- data/spec/support/friends_mapper.rb +29 -0
- data/spec/support/pet_mapper.rb +5 -0
- data/spec/support/pet_peeve_mapper.rb +3 -0
- data/spec/support/serializers.rb +11 -11
- data/spec/support/shared_contexts.rb +47 -0
- data/spec/support/shorthands.rb +22 -0
- data/spec/yaks/collection_resource_spec.rb +9 -0
- data/spec/yaks/hal_serializer_spec.rb +9 -0
- data/spec/yaks/mapper/association_spec.rb +21 -0
- data/spec/yaks/mapper/class_methods_spec.rb +28 -0
- data/spec/yaks/mapper/config_spec.rb +77 -0
- data/spec/yaks/mapper/has_one_spec.rb +16 -0
- data/spec/yaks/mapper/link_spec.rb +42 -0
- data/spec/yaks/mapper/map_links_spec.rb +46 -0
- data/spec/yaks/mapper_spec.rb +47 -0
- data/spec/yaks/resource_spec.rb +23 -0
- data/yaks.gemspec +6 -3
- metadata +115 -27
- data/lib/yaks/dumper.rb +0 -23
- data/lib/yaks/fold_ams_compat.rb +0 -33
- data/lib/yaks/fold_json_api.rb +0 -61
- data/lib/yaks/serializable_association.rb +0 -21
- data/lib/yaks/serializable_collection.rb +0 -10
- data/lib/yaks/serializable_object.rb +0 -18
- data/lib/yaks/serializer/class_methods.rb +0 -76
- data/spec/integration_spec.rb +0 -57
- data/spec/yaks/json_api_folder_spec.rb +0 -63
- data/spec/yaks/serializer_spec.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NTZhMTliZGU4NTNkZTNmYjYyOGRhYzFjZjAwNTIwYTI4YTdjNDJjNA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MmVjN2NjZDI0ZDhjNjliYTczZjk3ODE0OTJmNGQzNDk3ZmE3ZDk0OQ==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
M2E1ZWY2MzIyMzkyMmIxMDM3ZTFhZGRkODhhNzhiMjM1NTZiMTkxMDVjZjE2
|
10
|
+
MTc5NTAwYWI4NGJiOWJjMjVkNGI5MjdmZmM3OWE3ODIwYWYwMDNmM2FkNGMy
|
11
|
+
ZTcxNTRkNGIzMjgxZWU4OTdlYzMyMmUwYmEwYjRlM2FhODA1YTc=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NjJhM2ExODNhNGM2OTAzMzMzZDc3MjlkNTM0NWVmM2I1N2VlMjdkZmYxZWU2
|
14
|
+
Mzg2MmQxZmYyNmZkZTJjODZlMDFiYjViZjA5OGVjMjI3ZDEyYzI3ZGEwZWUw
|
15
|
+
MDc3NWU0MDBiYTk5OWQ3ZGU3OTJkMTVlMzQ1MmUwNjllM2ExZjU=
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
language: ruby
|
2
|
+
before_install: gem install bundler
|
3
|
+
bundler_args: --without yard guard benchmarks
|
4
|
+
script: "bundle exec rspec"
|
5
|
+
rvm:
|
6
|
+
- 1.9.2
|
7
|
+
- 1.9.3
|
8
|
+
- 2.0.0
|
9
|
+
- 2.1.0
|
10
|
+
- ruby-head
|
11
|
+
- rbx-19mode
|
12
|
+
matrix:
|
13
|
+
include:
|
14
|
+
- rvm: jruby-19mode
|
15
|
+
env: JRUBY_OPTS="$JRUBY_OPTS --debug"
|
16
|
+
- rvm: jruby-head
|
17
|
+
env: JRUBY_OPTS="$JRUBY_OPTS --debug"
|
18
|
+
notifications:
|
19
|
+
email:
|
20
|
+
recipients:
|
21
|
+
- arne@arnebrasseur.net
|
22
|
+
on_success: never
|
23
|
+
on_failure: change
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git://github.com/harukizaemon/hamster.git
|
3
|
+
revision: aaf874b20bd31c2d984ae131dbf58d9a9bc2b379
|
4
|
+
specs:
|
5
|
+
hamster (0.4.3)
|
6
|
+
|
1
7
|
PATH
|
2
8
|
remote: .
|
3
9
|
specs:
|
4
|
-
yaks (0.
|
10
|
+
yaks (0.1.0)
|
5
11
|
concord (~> 0.1.4)
|
6
12
|
hamster (~> 0.4.3)
|
7
13
|
inflection (~> 1.0.0)
|
8
|
-
|
9
|
-
PATH
|
10
|
-
remote: /home/arne/github/hamster
|
11
|
-
specs:
|
12
|
-
hamster (0.4.3)
|
14
|
+
uri_template (~> 0.6.0)
|
13
15
|
|
14
16
|
GEM
|
15
17
|
remote: https://rubygems.org/
|
@@ -28,11 +30,23 @@ GEM
|
|
28
30
|
concord (0.1.4)
|
29
31
|
adamantium (~> 0.1)
|
30
32
|
equalizer (~> 0.0.7)
|
33
|
+
coveralls (0.7.0)
|
34
|
+
multi_json (~> 1.3)
|
35
|
+
rest-client
|
36
|
+
simplecov (>= 0.7)
|
37
|
+
term-ansicolor
|
38
|
+
thor
|
31
39
|
descendants_tracker (0.0.3)
|
32
40
|
diff-lcs (1.2.5)
|
41
|
+
docile (1.1.1)
|
33
42
|
equalizer (0.0.8)
|
34
43
|
ice_nine (0.10.0)
|
35
44
|
inflection (1.0.0)
|
45
|
+
mime-types (2.0)
|
46
|
+
multi_json (1.8.2)
|
47
|
+
rake (10.1.1)
|
48
|
+
rest-client (1.6.7)
|
49
|
+
mime-types (>= 1.16)
|
36
50
|
rspec (2.14.1)
|
37
51
|
rspec-core (~> 2.14.0)
|
38
52
|
rspec-expectations (~> 2.14.0)
|
@@ -41,8 +55,18 @@ GEM
|
|
41
55
|
rspec-expectations (2.14.4)
|
42
56
|
diff-lcs (>= 1.1.3, < 2.0)
|
43
57
|
rspec-mocks (2.14.4)
|
58
|
+
simplecov (0.8.2)
|
59
|
+
docile (~> 1.1.0)
|
60
|
+
multi_json
|
61
|
+
simplecov-html (~> 0.8.0)
|
62
|
+
simplecov-html (0.8.0)
|
63
|
+
term-ansicolor (1.2.2)
|
64
|
+
tins (~> 0.8)
|
65
|
+
thor (0.18.1)
|
44
66
|
thread_safe (0.1.3)
|
45
67
|
atomic
|
68
|
+
tins (0.13.1)
|
69
|
+
uri_template (0.6.0)
|
46
70
|
virtus (1.0.0)
|
47
71
|
axiom-types (~> 0.0.5)
|
48
72
|
coercible (~> 0.2)
|
@@ -53,7 +77,9 @@ PLATFORMS
|
|
53
77
|
ruby
|
54
78
|
|
55
79
|
DEPENDENCIES
|
80
|
+
coveralls
|
56
81
|
hamster!
|
82
|
+
rake
|
57
83
|
rspec
|
58
84
|
virtus
|
59
85
|
yaks!
|
data/README.md
CHANGED
@@ -1,105 +1,207 @@
|
|
1
|
-
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/yaks.png)][gem]
|
2
|
+
[![Build Status](https://secure.travis-ci.org/plexus/yaks.png?branch=master)][travis]
|
3
|
+
[![Dependency Status](https://gemnasium.com/plexus/yaks.png)][gemnasium]
|
4
|
+
[![Code Climate](https://codeclimate.com/github/plexus/yaks.png)][codeclimate]
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/plexus/yaks/badge.png?branch=master)][coveralls]
|
6
|
+
|
7
|
+
[gem]: https://rubygems.org/gems/yaks
|
8
|
+
[travis]: https://travis-ci.org/plexus/yaks
|
9
|
+
[gemnasium]: https://gemnasium.com/plexus/yaks
|
10
|
+
[codeclimate]: https://codeclimate.com/github/plexus/yaks
|
11
|
+
[coveralls]: https://coveralls.io/r/plexus/yaks
|
12
|
+
|
13
|
+
# Yaks
|
14
|
+
|
15
|
+
### One Stop Hypermedia Shopping ###
|
16
|
+
|
17
|
+
Yaks is a tool for turning your domain models into Hypermedia resources.
|
18
|
+
|
19
|
+
There are at the moment a number of competing media types for building Hypermedia APIs. These all add a layer of semantics on top of a low level serialization format such as JSON or XML. Even though they each have their own design goals, the core features mostly overlap. They typically provide a way to represent resources (entities), and resource collections, consisting of
|
20
|
+
|
21
|
+
* Data in key-value format, possibly with composite values
|
22
|
+
* Embedded resources
|
23
|
+
* Links to related resources
|
24
|
+
* Outbound links that have a specific relation to the resource
|
25
|
+
|
26
|
+
They might also contain extra control data to specify possible future interactions, not unlike HTML forms.
|
27
|
+
|
28
|
+
These different media types for Hypermedia clients and servers base themselves on the same set of internet standards, such as [RFC4288 Media types](http://tools.ietf.org/html/rfc4288), [RFC5988 Web Linking](http://tools.ietf.org/html/rfc5988), [RFC6906 The "profile" link relation](http://tools.ietf.org/search/rfc6906) and [RFC6570 URI Templates](http://tools.ietf.org/html/rfc6570).
|
29
|
+
|
30
|
+
## Yaks Resources
|
31
|
+
|
32
|
+
At the core of Yaks is the concept of a Resource, consisting of key-value attributes, RFC5988 style links, and embedded sub-resources.
|
33
|
+
|
34
|
+
To build an API you create 'mappers' that turn your domain models into resources. Then you pick a media type 'serializer', which can turn the resource into a hypermedia message.
|
35
|
+
|
36
|
+
The web linking standard which defines link relations like 'self', 'next' or 'alternate' is embraced as far as practically possible, for instance to find the URI that uniquely defines a resource, we look at the 'self' link. To distinguish different types of resources we use the 'profile' link.
|
37
|
+
|
38
|
+
## Mappers
|
39
|
+
|
40
|
+
To turn your domain models into resources, you define mappers, for example :
|
2
41
|
|
3
42
|
```ruby
|
4
|
-
class
|
5
|
-
|
43
|
+
class PostMapper < Yaks::Mapper
|
44
|
+
link :self, '/api/posts/{id}'
|
45
|
+
|
46
|
+
attributes :id, :title
|
47
|
+
|
48
|
+
has_one :author
|
49
|
+
has_many :comments
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Now you can use this to create a Resource
|
6
54
|
|
7
|
-
|
8
|
-
|
55
|
+
```ruby
|
56
|
+
resource = PostMapper.new(post).to_resource
|
57
|
+
```
|
9
58
|
|
10
|
-
|
11
|
-
|
59
|
+
### Attributes
|
60
|
+
|
61
|
+
Use the `attributes` DSL method to specify which attributes of your model you want to expose, as in the example above. You can override the `load_attribute` method to change how attributes are fetched from the model.
|
62
|
+
|
63
|
+
For example, if you are representing data that is stored in a Hash, you could do
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class PostHashMapper < Yaks::Mapper
|
67
|
+
attributes :id, :body
|
68
|
+
|
69
|
+
def load_attribute(name)
|
70
|
+
object[name]
|
12
71
|
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
The default implementation will first try to find a matching method for an attribute on the mapper itself, and will then fall back to calling the actual model. So you can add extra 'virtual' attributes like so :
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class CommentMapper < Yaks::Mapper
|
79
|
+
attributes :id, :body, :date
|
13
80
|
|
14
|
-
def
|
15
|
-
|
81
|
+
def date
|
82
|
+
object.created_at.strftime("at %I:%M%p")
|
16
83
|
end
|
17
84
|
end
|
85
|
+
```
|
86
|
+
|
87
|
+
#### Filtering
|
88
|
+
|
89
|
+
Implement `filter(attrs)` to filter out specific attributes, e.g. based on options.
|
18
90
|
|
19
|
-
|
20
|
-
|
91
|
+
```ruby
|
92
|
+
def filter(attrs)
|
93
|
+
attrs.reject{|attr| options[:exclude].include? attr
|
21
94
|
end
|
95
|
+
```
|
22
96
|
|
23
|
-
|
24
|
-
|
97
|
+
### Links
|
98
|
+
|
99
|
+
You can specify link templates that will be expanded with model attributes. The link relation name should be a registered [IANA link relation](http://www.iana.org/assignments/link-relations/link-relations.xhtml) or a URL.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class FooMapper < Yaks::Mapper
|
103
|
+
link :self, '/api/foo/{id}'
|
104
|
+
link 'http://api.foo.com/rels/comments', '/api/foo/{id}/comments'
|
25
105
|
end
|
106
|
+
```
|
107
|
+
|
108
|
+
To prevent a link to be expanded, add `expand: false` as an option. Now the actual template will be rendered in the result, so clients can use it to generate links from.
|
26
109
|
|
27
|
-
|
28
|
-
|
29
|
-
|
110
|
+
### Associations
|
111
|
+
|
112
|
+
Use `has_one` for an association that returns a single object, or `has_many` for embedding a collection.
|
113
|
+
|
114
|
+
Options
|
115
|
+
|
116
|
+
* `:as` : use a different name for the association in the result
|
117
|
+
* `:mapper` : Use a specific for each instance, will be derived from the class name if omitted (see Policy vs Configuration)
|
118
|
+
* `:collection_mapper` : For mapping the collection as a whole, this defaults to Yaks::CollectionMapper, but you can subclass it for example to add links or attributes on the collection itself
|
119
|
+
* `:policy` : supply an alternative Policy object
|
120
|
+
|
121
|
+
## Serializers
|
122
|
+
|
123
|
+
A resource can be turned in to a specific media type representation, for example [HAL](http://stateless.co/hal_specification.html) using a Serializer
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
hal = Yaks::HalSerializer.new(resource).serialize
|
127
|
+
puts JSON.dump(hal)
|
30
128
|
```
|
31
129
|
|
130
|
+
This will give you back a composite types consisting of primitives that have a mapping to JSON, so you can use your favorite JSON encoder to turn this into a character stream.
|
131
|
+
|
132
|
+
### Yaks::HalSerializer
|
133
|
+
|
134
|
+
Serializes to HAL. In HAL one decides when building an API which links can only be singular (e.g. self), and which are always represented as an array. So the HalSerializer understand the `:singular_links` option.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
hal = Yaks::HalSerializer.new(resource, singular_links: [:self, :"ea:find", :"ea:basket"])
|
138
|
+
```
|
139
|
+
|
140
|
+
CURIEs are not explicitly supported, but it's possible to use them with some effort, see `examples/hal01.rb` for an example.
|
141
|
+
|
142
|
+
The line between a singular resource and a collection is fuzzy in HAL. To stick close to the spec you're best to create your own singular types that represent collections, rather than rendering a top level CollectionResource.
|
143
|
+
|
144
|
+
### Yaks::JsonApiSerializer
|
145
|
+
|
146
|
+
JSON-API has no concept of outbound links, so these will not be rendered, but the profile link information will be used to derive the root key.
|
147
|
+
|
148
|
+
* `embed: :resources` : Embed resources in a `{"linked":` section, referenced by id
|
149
|
+
* `embed: :links` : Use URL style JSON-API
|
150
|
+
|
151
|
+
## Policy over Configuration
|
152
|
+
|
153
|
+
It's an old adagio in the Ruby/Rails world to have "Convention over Configuration", mostly to derive values that were not given explicitly. Typically based on things having similar names and a 1-1 derivable relationship.
|
154
|
+
|
155
|
+
This saves a lot of typing, but for the uninitiated it can also create confusion, the implicitness makes it hard to follow what's going on.
|
156
|
+
|
157
|
+
What's worse, is that often the Configuration part is skimmed over, making it very hard to deviate from the Golden Standard.
|
158
|
+
|
159
|
+
There is another old adagio, "Policy vs Mechanism". Implement the mechanisms, but don't dictate the policy.
|
160
|
+
|
161
|
+
In Yaks whenever missing values need to be inferred, like finding an unspecified mapper for a relation, this is handled by a policy object. The default is `Yaks::DefaultPolicy`, you can go there to find all the rules of inference. Subclass it and override to fit your needs, then pass it in to each mapper/serializer, they will pass it on to whatever objects they call.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
PostMapper.new(post, policy: MyPolicy.new)
|
165
|
+
```
|
166
|
+
|
167
|
+
## ProfileRegistry , RelationRegistry
|
168
|
+
|
169
|
+
...
|
170
|
+
|
171
|
+
## Future plans
|
172
|
+
|
173
|
+
* Collection+JSON
|
174
|
+
* Siren
|
175
|
+
* Examples on how to integrate with web frameworks
|
176
|
+
|
177
|
+
## Acknowledgment
|
178
|
+
|
179
|
+
The mapper syntax is largely borrowed from ActiveModel::Serializers, which in turn closely mimics the syntax of ActiveRecord models. It's a great concise syntax that still offers plenty of flexibility, so to not reinvent the wheel we stick to the existing syntax as far as practical, although there are several extensions and deviations.
|
180
|
+
|
181
|
+
## How to contribute
|
182
|
+
|
183
|
+
Run the tests, the examples, try it with your own stuff and leave your impressions in the issues. Or discuss on API-craft.
|
184
|
+
|
185
|
+
To fix a bug
|
186
|
+
|
187
|
+
1. Fork the repo
|
188
|
+
2. Fix the bug, add tests for it
|
189
|
+
3. Push it to a named branch
|
190
|
+
4. Add a PR
|
191
|
+
|
192
|
+
To add a feature
|
193
|
+
|
194
|
+
1. Open an issue as soon as possible to gather feedback
|
195
|
+
2. Same as above, fork, push to named branch, make a pull-request
|
196
|
+
|
32
197
|
## Non-Features
|
33
198
|
|
34
199
|
* No core extensions
|
35
200
|
* Minimal dependencies
|
36
201
|
* Only serializes what explicitly has a Serializer, will never call to_json/as_json
|
37
202
|
* Adding extra output formats does not require altering existing code
|
203
|
+
* Has no opinion on what to use for final JSON encoding (json, multi_json, yajl, oj, etc.)
|
38
204
|
|
39
|
-
## Formats
|
40
|
-
|
41
|
-
* :json_api
|
42
|
-
|
43
|
-
```json
|
44
|
-
{
|
45
|
-
"shows" : [
|
46
|
-
{
|
47
|
-
"dates" : [ "next Sunday" ],
|
48
|
-
"name" : "Piglet gets his groove back",
|
49
|
-
"id" : 5,
|
50
|
-
"description" : "Once in a lifetime...",
|
51
|
-
"links" : {
|
52
|
-
"event_category" : 2,
|
53
|
-
"events" : [ 7 ]
|
54
|
-
}
|
55
|
-
}
|
56
|
-
],
|
57
|
-
"linked" : {
|
58
|
-
"event_categories" : [
|
59
|
-
{
|
60
|
-
"name" : "Drama",
|
61
|
-
"id" : 2
|
62
|
-
}
|
63
|
-
],
|
64
|
-
"events" : [
|
65
|
-
{
|
66
|
-
"name" : "Sneak preview",
|
67
|
-
"id" : 7
|
68
|
-
}
|
69
|
-
]
|
70
|
-
}
|
71
|
-
}
|
72
|
-
```
|
73
|
-
|
74
|
-
* :ams_compat
|
75
|
-
|
76
|
-
```json
|
77
|
-
{
|
78
|
-
"shows" : [
|
79
|
-
{
|
80
|
-
"dates" : [ "next Sunday" ],
|
81
|
-
"name" : "Piglet gets his groove back",
|
82
|
-
"id" : 5,
|
83
|
-
"description" : null,
|
84
|
-
"event_ids" : [ 7 ],
|
85
|
-
"event_category_id" : 2
|
86
|
-
}
|
87
|
-
],
|
88
|
-
"event_categories" : [{
|
89
|
-
"name" : "Drama",
|
90
|
-
"id" : 2
|
91
|
-
}],
|
92
|
-
"events" : [{
|
93
|
-
"Name" : "Sneak preview",
|
94
|
-
"id" : 7
|
95
|
-
}]
|
96
|
-
}
|
97
|
-
|
98
|
-
## Maturity
|
99
|
-
|
100
|
-
Infantile. Crazy what these kids get up to, though.
|
101
|
-
|
102
|
-
requires current master of Hamster.
|
103
205
|
|
104
206
|
## License
|
105
207
|
|
data/examples/hal01.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# http://stateless.co/hal_specification.html
|
2
|
+
|
3
|
+
# There are still some affordances missing to support all of HAL, in particular
|
4
|
+
# for this example to ability to generate links based on composite content
|
5
|
+
# (ea:admin) is not implemented, neither is explicit support for CURIEs (compact
|
6
|
+
# URI shorthand syntax), although this examples works around that by manually
|
7
|
+
# encoding the CURIE prefixes.
|
8
|
+
|
9
|
+
# The 'next' link from the example below is also ommitted, pagination is an aspect
|
10
|
+
# that will be implemented generically
|
11
|
+
|
12
|
+
# Example from the specification, approximated with Yaks below
|
13
|
+
|
14
|
+
# {
|
15
|
+
# "_links": {
|
16
|
+
# "self": { "href": "/orders" },
|
17
|
+
# "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
|
18
|
+
# "next": { "href": "/orders?page=2" },
|
19
|
+
# "ea:find": { "href": "/orders{?id}", "templated": true },
|
20
|
+
# "ea:admin": [{
|
21
|
+
# "href": "/admins/2",
|
22
|
+
# "title": "Fred"
|
23
|
+
# }, {
|
24
|
+
# "href": "/admins/5",
|
25
|
+
# "title": "Kate"
|
26
|
+
# }]
|
27
|
+
# },
|
28
|
+
# "currentlyProcessing": 14,
|
29
|
+
# "shippedToday": 20,
|
30
|
+
# "_embedded": {
|
31
|
+
# "ea:order": [{
|
32
|
+
# "_links": {
|
33
|
+
# "self": { "href": "/orders/123" },
|
34
|
+
# "ea:basket": { "href": "/baskets/98712" },
|
35
|
+
# "ea:customer": { "href": "/customers/7809" }
|
36
|
+
# },
|
37
|
+
# "total": 30.00,
|
38
|
+
# "currency": "USD",
|
39
|
+
# "status": "shipped"
|
40
|
+
# }, {
|
41
|
+
# "_links": {
|
42
|
+
# "self": { "href": "/orders/124" },
|
43
|
+
# "ea:basket": { "href": "/baskets/97213" },
|
44
|
+
# "ea:customer": { "href": "/customers/12369" }
|
45
|
+
# },
|
46
|
+
# "total": 20.00,
|
47
|
+
# "currency": "USD",
|
48
|
+
# "status": "processing"
|
49
|
+
# }]
|
50
|
+
# }
|
51
|
+
# }
|
52
|
+
|
53
|
+
require 'virtus'
|
54
|
+
require 'yaks'
|
55
|
+
require 'json'
|
56
|
+
|
57
|
+
class Order
|
58
|
+
include Virtus.model
|
59
|
+
attribute :id, Integer
|
60
|
+
attribute :basket_id, Integer
|
61
|
+
attribute :customer_id, Integer
|
62
|
+
attribute :total, Numeric
|
63
|
+
attribute :currency, String
|
64
|
+
attribute :status, String
|
65
|
+
end
|
66
|
+
|
67
|
+
class OrderSet
|
68
|
+
include Virtus.model
|
69
|
+
attribute :currently_processing, Integer
|
70
|
+
attribute :shipped_today, Integer
|
71
|
+
attribute :orders, Array[Order]
|
72
|
+
end
|
73
|
+
|
74
|
+
class OrderMapper < Yaks::Mapper
|
75
|
+
link :self, '/orders/{id}'
|
76
|
+
link :"ea:basket", '/baskets/{basket_id}'
|
77
|
+
link :"ea:customer", '/customers/{customer_id}'
|
78
|
+
|
79
|
+
attributes :total, :currency, :status
|
80
|
+
end
|
81
|
+
|
82
|
+
class OrderSetMapper < Yaks::Mapper
|
83
|
+
link :self, '/orders'
|
84
|
+
link :curies, 'http://example.com/docs/rels/{rel}', name: "ea", expand: false
|
85
|
+
link :"ea:find", "/orders{?id}", expand: false
|
86
|
+
|
87
|
+
attributes :currentlyProcessing, :shippedToday
|
88
|
+
|
89
|
+
has_many :orders, as: :"ea:order", mapper: OrderMapper
|
90
|
+
|
91
|
+
# Having the attributes be encoded in CamelCase is such a common
|
92
|
+
# use case we might have to make this a setting
|
93
|
+
|
94
|
+
def load_attribute(name)
|
95
|
+
super(Yaks::Util.underscore(name.to_s))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
order_set = OrderSet.new(
|
100
|
+
currently_processing: 14,
|
101
|
+
shipped_today: 20,
|
102
|
+
orders: [
|
103
|
+
Order.new(
|
104
|
+
id: 123,
|
105
|
+
basket_id: 98712,
|
106
|
+
customer_id: 7809,
|
107
|
+
total: 30.00,
|
108
|
+
currency: "USD",
|
109
|
+
status: "shipped"
|
110
|
+
),
|
111
|
+
Order.new(
|
112
|
+
id: 124,
|
113
|
+
basket_id: 97213,
|
114
|
+
customer_id: 12369,
|
115
|
+
total: 20.00,
|
116
|
+
currency: "USD",
|
117
|
+
status: "processing"
|
118
|
+
)
|
119
|
+
]
|
120
|
+
)
|
121
|
+
|
122
|
+
resource = OrderSetMapper.new(order_set).to_resource
|
123
|
+
|
124
|
+
hal = Yaks::HalSerializer.new(resource, singular_links: [:self, :"ea:find", :"ea:basket", :"ea:customer"]).to_hal
|
125
|
+
|
126
|
+
puts JSON.dump(hal)
|