yaks 0.5.0 → 0.6.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +20 -9
- data/CHANGELOG.md +2 -3
- data/Gemfile +2 -1
- data/Rakefile +29 -50
- data/yaks/README.md +526 -0
- data/{lib → yaks/lib}/yaks/breaking_changes.rb +0 -0
- data/{lib → yaks/lib}/yaks/collection_mapper.rb +0 -0
- data/{lib → yaks/lib}/yaks/collection_resource.rb +0 -0
- data/{lib → yaks/lib}/yaks/config/dsl.rb +0 -0
- data/{lib → yaks/lib}/yaks/config.rb +0 -0
- data/{lib → yaks/lib}/yaks/default_policy.rb +2 -1
- data/{lib → yaks/lib}/yaks/format/collection_json.rb +0 -0
- data/{lib → yaks/lib}/yaks/format/hal.rb +0 -0
- data/{lib → yaks/lib}/yaks/format/json_api.rb +0 -0
- data/{lib → yaks/lib}/yaks/format.rb +0 -0
- data/{lib → yaks/lib}/yaks/fp/callable.rb +0 -0
- data/{lib → yaks/lib}/yaks/fp/hash_updatable.rb +0 -0
- data/{lib → yaks/lib}/yaks/fp/updatable.rb +0 -0
- data/{lib → yaks/lib}/yaks/fp.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/association.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/association_mapper.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/attribute.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/class_methods.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/config.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/has_many.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/has_one.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper/link.rb +0 -0
- data/{lib → yaks/lib}/yaks/mapper.rb +0 -0
- data/{lib → yaks/lib}/yaks/null_resource.rb +0 -0
- data/{lib → yaks/lib}/yaks/primitivize.rb +0 -0
- data/{lib → yaks/lib}/yaks/resource/link.rb +0 -0
- data/{lib → yaks/lib}/yaks/resource.rb +0 -0
- data/{lib → yaks/lib}/yaks/runner.rb +12 -1
- data/{lib → yaks/lib}/yaks/util.rb +0 -0
- data/yaks/lib/yaks/version.rb +3 -0
- data/{lib → yaks/lib}/yaks.rb +0 -0
- data/{spec → yaks/spec}/acceptance/acceptance_spec.rb +0 -0
- data/{spec → yaks/spec}/acceptance/json_shared_examples.rb +0 -0
- data/{spec → yaks/spec}/acceptance/models.rb +0 -0
- data/{spec → yaks/spec}/fixture_helpers.rb +0 -0
- data/{spec → yaks/spec}/integration/map_to_resource_spec.rb +0 -0
- data/{spec → yaks/spec}/json/confucius.collection.json +0 -0
- data/{spec → yaks/spec}/json/confucius.hal.json +0 -0
- data/{spec → yaks/spec}/json/confucius.json_api.json +0 -0
- data/{spec → yaks/spec}/json/john.hal.json +0 -0
- data/{spec → yaks/spec}/json/plant_collection.collection.json +0 -0
- data/{spec → yaks/spec}/json/plant_collection.hal.json +0 -0
- data/{spec → yaks/spec}/json/youtypeitwepostit.collection.json +0 -0
- data/{spec → yaks/spec}/spec_helper.rb +0 -0
- data/{spec → yaks/spec}/support/classes_for_policy_testing.rb +0 -0
- data/{spec → yaks/spec}/support/deep_eql.rb +0 -0
- data/{spec → yaks/spec}/support/fixtures.rb +0 -0
- data/{spec → yaks/spec}/support/friends_mapper.rb +0 -0
- data/{spec → yaks/spec}/support/models.rb +0 -0
- data/{spec → yaks/spec}/support/pet_mapper.rb +0 -0
- data/{spec → yaks/spec}/support/pet_peeve_mapper.rb +0 -0
- data/{spec → yaks/spec}/support/shared_contexts.rb +0 -0
- data/{spec → yaks/spec}/support/youtypeit_models_mappers.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/collection_mapper_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/collection_resource_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/config/dsl_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/config_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/default_policy/derive_mapper_from_object_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/default_policy_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/format/collection_json_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/format/hal_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/format/json_api_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/format_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/fp/callable_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/fp/hash_updatable_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/fp/updatable_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/fp_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/association_mapper_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/association_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/attribute_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/class_methods_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/config_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/has_many_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/has_one_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper/link_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/mapper_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/null_resource_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/primitivize_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/resource/link_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/resource_spec.rb +0 -0
- data/{spec → yaks/spec}/unit/yaks/runner_spec.rb +1 -1
- data/{spec → yaks/spec}/unit/yaks/util_spec.rb +0 -0
- data/{spec → yaks/spec}/yaml/confucius.yaml +0 -0
- data/{spec → yaks/spec}/yaml/youtypeitwepostit.yaml +0 -0
- data/{yaks.gemspec → yaks/yaks.gemspec} +0 -0
- data/yaks-html/README.md +3 -0
- data/yaks-html/lib/yaks/format/html.rb +70 -0
- data/yaks-html/lib/yaks/format/template.html +56 -0
- data/yaks-html/lib/yaks-html.rb +3 -0
- data/yaks-html/spec/spec_helper.rb +6 -0
- data/yaks-html/yaks-html.gemspec +22 -0
- metadata +99 -164
- data/lib/yaks/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d43a0d564f2fe2f4f074611448fdc3158211aa76
|
4
|
+
data.tar.gz: b15fa02453e1b71c6de06ac9fc157dddf09be27d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 115a1a00edaf48376c53e47b63ac3c2c96c0eec27fa6cba627c3e455320b5c36a1c6c331f834d08f5718fd991365cfb96ae8c46139df5f17f8cff38326a475f3
|
7
|
+
data.tar.gz: 03103204631478678810e10eccaca2cd66ed9bafbb3b25cf891b8f23e4aab01ed1b2493c918ebf3622f1a20d32d34e42476d8c3ac4b8fdea52752bd557d49066
|
data/.travis.yml
CHANGED
@@ -1,25 +1,36 @@
|
|
1
1
|
language: ruby
|
2
|
-
script: bundle exec $
|
2
|
+
script: bundle exec rake $TASK
|
3
3
|
rvm:
|
4
4
|
- 1.9.3
|
5
|
-
- 2.0
|
6
|
-
- 2.1
|
5
|
+
- 2.0
|
6
|
+
- 2.1
|
7
7
|
- ruby-head
|
8
8
|
- rbx-2
|
9
9
|
- jruby
|
10
10
|
- jruby-head
|
11
11
|
env:
|
12
|
-
-
|
13
|
-
-
|
12
|
+
- TASK=yaks:rspec
|
13
|
+
- TASK=yaks:mutant
|
14
|
+
- TASK=yaks-html:rspec
|
15
|
+
- TASK=yaks-html:mutant
|
14
16
|
matrix:
|
15
17
|
allow_failures:
|
16
18
|
- rvm: ruby-head
|
17
19
|
- rvm: jruby-head
|
18
|
-
- env:
|
20
|
+
- env: TASK=yaks:mutant
|
21
|
+
- env: TASK=yaks-html:mutant
|
19
22
|
exclude:
|
20
23
|
- rvm: jruby
|
21
|
-
env:
|
24
|
+
env: TASK=yaks:mutant
|
25
|
+
- rvm: jruby
|
26
|
+
env: TASK=yaks-html:mutant
|
27
|
+
|
28
|
+
- rvm: jruby-head
|
29
|
+
env: TASK=yaks:mutant
|
22
30
|
- rvm: jruby-head
|
23
|
-
env:
|
31
|
+
env: TASK=yaks-html:mutant
|
32
|
+
|
33
|
+
- rvm: rbx-2
|
34
|
+
env: TASK=yaks:mutant
|
24
35
|
- rvm: rbx-2
|
25
|
-
env:
|
36
|
+
env: TASK=yaks-html:mutant
|
data/CHANGELOG.md
CHANGED
@@ -23,12 +23,11 @@ end
|
|
23
23
|
|
24
24
|
* Mapping a non-empty collection will try to infer the type, and hence rel of the nested items, based on the first object in the collection. This is only relevant for formats like HAL that don't have a top-level collection representation, and only matters when mapping a collection at the top level, not when mapping a collection from an association.
|
25
25
|
|
26
|
+
* Collection+JSON uses a link's "title" attribute to output a link's "name", to better correspond with other formats
|
27
|
+
|
26
28
|
* When registering a custom format (Yaks::Format subclass), the signature has changed
|
27
29
|
|
28
30
|
``` ruby
|
29
|
-
|
30
|
-
* Collection+JSON uses a link's "title" attribute to output a link's "name", to better correspond with other formats
|
31
|
-
|
32
31
|
# 0.4.3
|
33
32
|
Format.register self, :collection_json, 'application/vnd.collection+json'
|
34
33
|
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
@@ -1,64 +1,43 @@
|
|
1
|
-
require 'rubygems/package_task'
|
2
1
|
require 'yaks'
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
require 'yaks-html'
|
3
|
+
require 'mutant'
|
4
|
+
require 'rubygems/package_task'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
require 'yard'
|
6
7
|
|
7
8
|
desc "Push gem to rubygems.org"
|
8
|
-
task :push => :gem do
|
9
|
+
task :push => ["yaks:gem", "yaks-html:gem"] do
|
9
10
|
sh "git tag v#{Yaks::VERSION}"
|
10
11
|
sh "git push --tags"
|
11
12
|
sh "gem push pkg/yaks-#{Yaks::VERSION}.gem"
|
13
|
+
sh "gem push pkg/yaks-html-#{Yaks::VERSION}.gem"
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
+
def gem_tasks(gem)
|
17
|
+
namespace gem do
|
18
|
+
spec = Gem::Specification.load("#{gem}/#{gem}.gemspec")
|
19
|
+
Gem::PackageTask.new(spec).define
|
16
20
|
|
17
|
-
task :mutant do
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
task :mutant do
|
22
|
+
pattern = ENV.fetch('PATTERN', gem == :yaks ? 'Yaks*' : 'Yaks::Format::HTML*')
|
23
|
+
opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
|
24
|
+
Dir.chdir gem.to_s do
|
25
|
+
result = Mutant::CLI.run(%W[-Ilib -ryaks --use rspec --score 100] + opts + [pattern])
|
26
|
+
fail unless result == Mutant::CLI::EXIT_SUCCESS
|
27
|
+
end
|
28
|
+
end
|
23
29
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# Yaks::Primitivize,
|
28
|
-
Yaks::FP,
|
29
|
-
Yaks::Resource,
|
30
|
-
Yaks::NullResource,
|
31
|
-
Yaks::CollectionResource,
|
32
|
-
Yaks::Mapper::Association,
|
33
|
-
Yaks::Mapper::AssociationMapper,
|
34
|
-
Yaks::Mapper::HasMany,
|
35
|
-
Yaks::Mapper::HasOne,
|
36
|
-
Yaks::Mapper::Config,
|
37
|
-
Yaks::Mapper::ClassMethods,
|
38
|
-
Yaks::Mapper::Attribute,
|
39
|
-
Yaks::Format,
|
40
|
-
Yaks::Config::DSL,
|
41
|
-
Yaks::CollectionMapper,
|
42
|
-
Yaks::Mapper::Link,
|
43
|
-
Yaks::Format::JsonApi,
|
44
|
-
Yaks::DefaultPolicy, # 45/249 (81.93%)
|
45
|
-
Yaks::Format::CollectionJson, # 15/183 (91.80%)
|
46
|
-
Yaks::Format::Hal, # 17/209 (91.87%)
|
47
|
-
Yaks::Mapper, # 12/203 (94.09%)
|
48
|
-
Yaks::Config, # 12/263 (95.44%)
|
49
|
-
].each do |space|
|
50
|
-
puts space
|
51
|
-
ENV['PATTERN'] = "#{space}"
|
52
|
-
Rake::Task["mutant"].execute
|
53
|
-
end
|
54
|
-
end
|
30
|
+
RSpec::Core::RakeTask.new(:rspec) do |t, task_args|
|
31
|
+
t.rspec_opts = "-I#{gem}/spec #{gem}/spec"
|
32
|
+
end
|
55
33
|
|
56
|
-
begin
|
57
|
-
require 'yard'
|
58
34
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
$stderr.puts 'In order to run yard, you must: gem install yard'
|
35
|
+
YARD::Rake::YardocTask.new do |t|
|
36
|
+
t.files = ["#{gem}/lib/**/*.rb" "#{gem}/**/*.md"]
|
37
|
+
end
|
63
38
|
end
|
39
|
+
|
64
40
|
end
|
41
|
+
|
42
|
+
gem_tasks(:yaks)
|
43
|
+
gem_tasks(:"yaks-html")
|
data/yaks/README.md
ADDED
@@ -0,0 +1,526 @@
|
|
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
|
+
|
6
|
+
[gem]: https://rubygems.org/gems/yaks
|
7
|
+
[travis]: https://travis-ci.org/plexus/yaks
|
8
|
+
[gemnasium]: https://gemnasium.com/plexus/yaks
|
9
|
+
[codeclimate]: https://codeclimate.com/github/plexus/yaks
|
10
|
+
|
11
|
+
# Yak Serializers
|
12
|
+
|
13
|
+
### One Stop Hypermedia Shopping ###
|
14
|
+
|
15
|
+
*We did the shaving for you*
|
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
|
+
## Concepts
|
31
|
+
|
32
|
+
Yaks is a processing pipeline, you create and configure the pipeline, then feed data through it.
|
33
|
+
|
34
|
+
``` ruby
|
35
|
+
yaks = Yaks.new do
|
36
|
+
default_format :hal
|
37
|
+
rel_template 'http://api.example.com/rels/{rel}'
|
38
|
+
format_options(:hal, plural_links: [:copyright])
|
39
|
+
namespace ::MyAPI
|
40
|
+
json_serializer do |data|
|
41
|
+
MultiJson.dump(data)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
yaks.call(data) # => JSON
|
46
|
+
```
|
47
|
+
|
48
|
+
Yaks performs this serialization in three steps
|
49
|
+
|
50
|
+
* It *maps* your data to a `Yaks::Resource`
|
51
|
+
* It *formats* the resource to a syntax tree representation
|
52
|
+
* It *serializes* to get the final output
|
53
|
+
|
54
|
+
For JSON types, the "syntax tree" is just a combination of Ruby primitives, nested arrays and hashes with strings, numbers, booleans, nils.
|
55
|
+
|
56
|
+
A Resource is an abstraction shared by all output formats. It can contain key-value attributes, RFC5988 style links, and embedded sub-resources.
|
57
|
+
|
58
|
+
To build an API you create a "mapper" for each type of object you want to represent. Yaks takes care of the rest.
|
59
|
+
|
60
|
+
For all configuration options see [Yaks::Config::DSL](http://rdoc.info/gems/yaks/frames/Yaks/Config/DSL).
|
61
|
+
|
62
|
+
See also the [API Docs on rdoc.info](http://rdoc.info/gems/yaks/frames/file/README.md)
|
63
|
+
|
64
|
+
## Mappers
|
65
|
+
|
66
|
+
Say your app has a `Post` object for blog posts. To serve posts over your API, define a `PostMapper`
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class PostMapper < Yaks::Mapper
|
70
|
+
link :self, '/api/posts/{id}'
|
71
|
+
|
72
|
+
attributes :id, :title
|
73
|
+
|
74
|
+
has_one :author
|
75
|
+
has_many :comments
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
Configure a Yaks instance and start serializing!
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
yaks = Yaks.new
|
83
|
+
yaks.call(post)
|
84
|
+
```
|
85
|
+
|
86
|
+
or a bit more elaborate
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
yaks = Yaks.new do
|
90
|
+
default_format :json_api
|
91
|
+
rel_template 'http://api.example.com/rels/{rel}'
|
92
|
+
format_options(:hal, plural_links: [:copyright])
|
93
|
+
end
|
94
|
+
|
95
|
+
yaks.call(post, mapper: PostMapper, format: :hal)
|
96
|
+
```
|
97
|
+
|
98
|
+
### Attributes
|
99
|
+
|
100
|
+
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.
|
101
|
+
|
102
|
+
For example, if you are representing data that is stored in a Hash, you could do
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class PostHashMapper < Yaks::Mapper
|
106
|
+
attributes :id, :body
|
107
|
+
|
108
|
+
# @param name [Symbol]
|
109
|
+
def load_attribute(name)
|
110
|
+
object[name]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
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 :
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class CommentMapper < Yaks::Mapper
|
119
|
+
attributes :id, :body, :date
|
120
|
+
|
121
|
+
def date
|
122
|
+
object.created_at.strftime("at %I:%M%p")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
#### Filtering
|
128
|
+
|
129
|
+
You can override `#attributes`, or `#associations`.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class SongMapper
|
133
|
+
attributes :title, :duration, :lyrics
|
134
|
+
|
135
|
+
has_one :artist
|
136
|
+
has_one :album
|
137
|
+
|
138
|
+
def minimal?
|
139
|
+
env['HTTP_PREFER'] =~ /minimal/
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return Array<Yaks::Mapper::Attribute>
|
143
|
+
def attributes
|
144
|
+
return super.reject {|attr| attr.name.equal? :lyrics } if minimal?
|
145
|
+
super
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return Array<Yaks::Mapper::Association>
|
149
|
+
def associations
|
150
|
+
return [] if minimal?
|
151
|
+
super
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Links
|
157
|
+
|
158
|
+
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. The template syntax follows [RFC6570 URI templates](http://tools.ietf.org/html/rfc6570).
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class FooMapper < Yaks::Mapper
|
162
|
+
link :self, '/api/foo/{id}'
|
163
|
+
link 'http://api.foo.com/rels/comments', '/api/foo/{id}/comments'
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
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.
|
168
|
+
|
169
|
+
To partially expand the template, pass an array with field names to expand. e.g.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class ProductMapper < Yaks::Mapper
|
173
|
+
link 'http://api.foo.com/rels/line_item', '/api/line_items?product_id={product_id}&quantity={quantity}', expand: [:product_id]
|
174
|
+
end
|
175
|
+
|
176
|
+
# "_links": {
|
177
|
+
# "http://api.foo.com/rels/line_item": {
|
178
|
+
# "href": "/api/line_items?product_id=273&quantity={quantity}",
|
179
|
+
# "templated": true
|
180
|
+
# }
|
181
|
+
# }
|
182
|
+
|
183
|
+
```
|
184
|
+
|
185
|
+
You can pass a symbol instead of a template, in that case the symbol will be used as a method name on the object to retrieve the link. You can override this behavior just like with attributes.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class FooMapper < Yaks::Mapper
|
189
|
+
link 'http://api.foo.com/rels/go_home', :home_url
|
190
|
+
# by default calls object.home_url
|
191
|
+
|
192
|
+
def home_url
|
193
|
+
object.setting('home_url')
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
### Associations
|
199
|
+
|
200
|
+
Use `has_one` for an association that returns a single object, or `has_many` for embedding a collection.
|
201
|
+
|
202
|
+
Options
|
203
|
+
|
204
|
+
* `:mapper` : Use a specific for each instance, will be derived from the class name if omitted (see Policy vs Configuration)
|
205
|
+
* `: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
|
206
|
+
* `:rel` : Set the relation (symbol or URI) this association has with the object. Will be derived from the association name and the configured rel_template if ommitted
|
207
|
+
* `:link_if`: Conditionally render the association as a link. A `:href` option is required
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
class ShowMapper < Yaks::Mapper
|
211
|
+
has_many :events, href: '/show/{id}/events', link_if: ->{ events.count > 50 }
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
## Calling Yaks
|
216
|
+
|
217
|
+
Once you have a Yaks instance, you can call it with `call`
|
218
|
+
(`serialize` also works but might be deprecated in the future.) Pass
|
219
|
+
it the data to be serialized, plus options.
|
220
|
+
|
221
|
+
* `:env` a Rack environment, see next section
|
222
|
+
* `:format` the format to be used, e.g. `:json_api`. Note that if the Rack env contains an `Accept` header which resolves to a recognized format, then the header takes precedence
|
223
|
+
* `:mapper` the mapper to be used. Will be inferred if omitted
|
224
|
+
* `:item_mapper` When rendering a collection, the mapper to be used for each item in the collection. Will be inferred from the class of the first item in the collection if omitted.
|
225
|
+
|
226
|
+
### Rack env
|
227
|
+
|
228
|
+
When serializing, Yaks lets you pass in an `env` hash, which will be made available to all mappers.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
yaks = Yaks.new
|
232
|
+
yaks.call(foo, env: my_env)
|
233
|
+
|
234
|
+
class FooMapper
|
235
|
+
attributes :bar
|
236
|
+
|
237
|
+
def bar
|
238
|
+
if env['something']
|
239
|
+
#...
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
The env hash will be available to all mappers, so you can use this to pass around context. In particular context related to the current HTTP request, e.g. the current logged in user, which is why the recommended use is to pass in the Rack environment.
|
246
|
+
|
247
|
+
If `env` contains a `HTTP_ACCEPT` key (Rack's way of representing the `Accept` header), Yaks will return the format that most closely matches what was requested.
|
248
|
+
|
249
|
+
## Namespace
|
250
|
+
|
251
|
+
Yaks by default will find your mappers for you if they follow the naming convention of appending 'Mapper' to the model class name. This (and all other "conventions") can be easily redefined though, see below. If you have your mappers inside a module, use `namespace`.
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
module API
|
255
|
+
module Mappers
|
256
|
+
class PostMapper < Yaks::Mapper
|
257
|
+
#...
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
yaks = Yaks.new do
|
263
|
+
namespace API::Mappers
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
If your namespace contains a `CollectionMapper`, Yaks will use that instead of `Yaks::CollectionMapper`, e.g.
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
module API
|
271
|
+
module Mappers
|
272
|
+
class CollectionMapper < Yaks::CollectionMapper
|
273
|
+
link :profile, 'http://api.example.com/profiles/collection'
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
You can also have collection mappers based on the type of members the collection holds, e.g.
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
module API
|
283
|
+
module Mappers
|
284
|
+
class LineItemCollectionMapper < Yaks::CollectionMapper
|
285
|
+
link :profile, 'http://api.example.com/profiles/line_items'
|
286
|
+
attributes :total
|
287
|
+
|
288
|
+
def total
|
289
|
+
collection.inject(0) do |memo, line_item|
|
290
|
+
memo + line_item.price * line_item.quantity
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
Yaks will automatically detect and use this collection when serializing an array of `LineItem` objects.
|
299
|
+
|
300
|
+
|
301
|
+
## Custom attribute/link/subresource handling
|
302
|
+
|
303
|
+
When inheriting from `Yaks::Mapper`, you can override `map_attributes`, `map_links` and `map_resources` to skip (or augment) above methods, and instead implement your own custom mechanism. These methods take a `Yaks::Resource` instance, and should return an updated resource. They should not alter the resource instance in-place. For example
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
class ErrorMapper < Yaks::Mapper
|
307
|
+
link :profile, '/api/error'
|
308
|
+
|
309
|
+
def map_attributes(resource)
|
310
|
+
attrs = {
|
311
|
+
http_code: 500,
|
312
|
+
message: object.to_s,
|
313
|
+
type: object.class.name.underscore
|
314
|
+
}
|
315
|
+
|
316
|
+
case object
|
317
|
+
when AllocationException
|
318
|
+
attrs[:http_code] = 422
|
319
|
+
when ActiveRecord::RecordNotFound
|
320
|
+
attrs[:http_code] = 404
|
321
|
+
attrs[:type] = "record_not_found"
|
322
|
+
end
|
323
|
+
|
324
|
+
resource.update_attributes(attrs)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
## Resources and Serializers
|
330
|
+
|
331
|
+
Yaks uses an intermediate "Resource" representation to support multiple output formats. A mapper turns a domain model into a `Yaks::Resource`. A serializer (e.g. `Yaks::Serializer::Hal`) takes the resource and outputs the structure of the target format.
|
332
|
+
|
333
|
+
Since version 0.4 the recommended API is through `Yaks.new {...}.serialize`. This will give you back a composite value consisting of primitives that have a mapping to JSON, so you can use your favorite JSON encoder to turn this into a character stream.
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
my_yaks = Yaks.new
|
337
|
+
hal = my_yaks.call(model)
|
338
|
+
puts JSON.dump(hal)
|
339
|
+
```
|
340
|
+
|
341
|
+
There are at least a handful of JSON libraries and implementations for Ruby out there, with different trade-offs. Yaks does not impose an opinion on which one to use
|
342
|
+
|
343
|
+
### HAL
|
344
|
+
|
345
|
+
This is the default. 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. Yaks defaults to singular as I've found it to be the most common case. If you want specific links to be plural, then configure their rel href as such.
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
hal = Yaks.new do
|
349
|
+
format_options :hal, plural_links: ['http://api.example.com/rels/foo']
|
350
|
+
end
|
351
|
+
```
|
352
|
+
|
353
|
+
CURIEs are not explicitly supported (yet), but it's possible to use them with some effort, see `examples/hal01.rb` for an example.
|
354
|
+
|
355
|
+
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.
|
356
|
+
|
357
|
+
### JSON-API
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
default_format :json_api
|
361
|
+
```
|
362
|
+
|
363
|
+
JSON-API has no concept of outbound links, so these will not be rendered. Instead the key will be inferred from the mapper class name by default. This can be changed per mapper:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class AnimalMapper
|
367
|
+
key :pet
|
368
|
+
end
|
369
|
+
```
|
370
|
+
|
371
|
+
Or the policy can be overridden:
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
yaks = Yaks.new do
|
375
|
+
derive_type_from_mapper_class do |mapper_class|
|
376
|
+
piglatinize(mapper_class.to_s.sub(/Mapper$/, ''))
|
377
|
+
end
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
### Collection+JSON
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
default_format :collection_json
|
385
|
+
```
|
386
|
+
|
387
|
+
Subresources aren't mapped because Collection+JSON doesn't really have that concept, and the other way around templates and queries don't exist (yet) in Yaks.
|
388
|
+
|
389
|
+
### More formats
|
390
|
+
|
391
|
+
Are planned... at the moment HAL is the only format I actually use, so it's the one that's best supported. Adding formats that follow the resource=(attributes, links, subresources) structure or a subset thereof is straightforward. More features, e.g. forms/actions such as used in Mason might be added in the future.
|
392
|
+
|
393
|
+
## Hooks
|
394
|
+
|
395
|
+
It is possible to hook into the Yaks pipeline to perform extra processing steps before, after, or around each step. It also possible to skip a step.
|
396
|
+
|
397
|
+
``` ruby
|
398
|
+
yaks = Yaks.new do
|
399
|
+
# Automatically give every resource a self link
|
400
|
+
after :map, :add_self_link do |resource|
|
401
|
+
resource.add_link(Yaks::Resource::Link.new(:self, "/#{resource.type}/#{resource.attributes[:id]}"))
|
402
|
+
end
|
403
|
+
|
404
|
+
# Skip serialization, so Ruby primitives come back instead of JSON
|
405
|
+
# This was the default before versions < 0.5.0
|
406
|
+
skip :serialize
|
407
|
+
end
|
408
|
+
```
|
409
|
+
|
410
|
+
## Policy over Configuration
|
411
|
+
|
412
|
+
It's an old adage 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.
|
413
|
+
|
414
|
+
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.
|
415
|
+
|
416
|
+
What's worse, is that often the Configuration part is skipped entirely, making it very hard to deviate from the Golden Standard.
|
417
|
+
|
418
|
+
There is another old adage, "Policy vs Mechanism". Implement the mechanisms, but don't dictate the policy.
|
419
|
+
|
420
|
+
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. Single rules of inference can be redefined directly in the Yaks configuration:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
yaks = Yaks.new do
|
424
|
+
derive_mapper_from_object do |model|
|
425
|
+
# ...
|
426
|
+
end
|
427
|
+
|
428
|
+
derive_type_from_mapper_class do |mapper_class|
|
429
|
+
# ...
|
430
|
+
end
|
431
|
+
|
432
|
+
derive_mapper_from_association do |association|
|
433
|
+
# ...
|
434
|
+
end
|
435
|
+
|
436
|
+
derive_rel_from_association do |mapper, association|
|
437
|
+
# ...
|
438
|
+
end
|
439
|
+
end
|
440
|
+
```
|
441
|
+
|
442
|
+
You can also subclass or create from scratch your own policy class
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
class MyPolicy < DefaultPolicy
|
446
|
+
#...
|
447
|
+
end
|
448
|
+
|
449
|
+
yaks = Yaks.new do
|
450
|
+
policy MyPolicy
|
451
|
+
end
|
452
|
+
```
|
453
|
+
|
454
|
+
## Primitives
|
455
|
+
|
456
|
+
For JSON based formats, the "syntax tree" is merely a structure of Ruby primitives that have a JSON equivalent. If your mappers return non-primitive attribute values, you can define how they should be converted. For example, JSON has no notion of dates. If your mappers return these types as attributes, then Yaks needs to know how to turn these into primitives. To add extra types, use `map_to_primitive`
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
Yaks.new do
|
460
|
+
map_to_primitive Date, Time, DateTime do |date|
|
461
|
+
date.iso8601
|
462
|
+
end
|
463
|
+
end
|
464
|
+
```
|
465
|
+
|
466
|
+
This can also be used to transform alternative data structures, like those from Hamster, into Ruby arrays and hashes. Use `call()` to recursively turn things into primitives.
|
467
|
+
|
468
|
+
```ruby
|
469
|
+
Yaks.new do
|
470
|
+
map_to_primitive Hamster::Vector, Hamster::List do |list|
|
471
|
+
list.map do |item|
|
472
|
+
call(item)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
```
|
477
|
+
|
478
|
+
Yaks by default "primitivizes" symbols (as strings), and classes that include Enumerable (as arrays).
|
479
|
+
|
480
|
+
## Real World Usage
|
481
|
+
|
482
|
+
Yaks is used in production by [Ticketsolve](http://www.ticketsolve.com/). You can find an example API endpoint [here](http://leicestersquaretheatre.ticketsolve.com/api).
|
483
|
+
|
484
|
+
Get in touch if you like to see your name and API here.
|
485
|
+
|
486
|
+
## Demo
|
487
|
+
|
488
|
+
You can find an example app at [Yakports](https://github.com/plexus/yakports), or browse the HAL api directly using the [HAL browser](http://yaks-airports.herokuapp.com/browser.html).
|
489
|
+
|
490
|
+
## Acknowledgment
|
491
|
+
|
492
|
+
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 I've stuck to the existing syntax as far as practical, although there are several extensions and deviations.
|
493
|
+
|
494
|
+
## Lightweight
|
495
|
+
|
496
|
+
Yaks is a lean library. It only depends on a few other tiny libraries (inflection, concord, uri_template). It has no core extensions (monkey patches). There is deliberately no built-in "integration" with existing frameworks, since the API is simply enough. You just call it.
|
497
|
+
|
498
|
+
If this approach sounds appealing, have a look at [microrb.com](http://microrb.com/).
|
499
|
+
|
500
|
+
## Is it any good
|
501
|
+
|
502
|
+
[Yes](https://news.ycombinator.com/item?id=3067434)
|
503
|
+
|
504
|
+
## How to contribute
|
505
|
+
|
506
|
+
Run the tests, the examples, try it with your own stuff and leave your impressions in the issues. Or discuss on [API-craft](https://groups.google.com/d/forum/api-craft).
|
507
|
+
|
508
|
+
To fix a bug
|
509
|
+
|
510
|
+
1. Fork the repo
|
511
|
+
2. Fix the bug, add tests for it
|
512
|
+
3. Push it to a named branch
|
513
|
+
4. Add a PR
|
514
|
+
|
515
|
+
To add a feature
|
516
|
+
|
517
|
+
1. Open an issue as soon as possible to gather feedback
|
518
|
+
2. Same as above, fork, push to named branch, make a pull-request
|
519
|
+
|
520
|
+
Yaks uses [Mutation Testing](https://github.com/mbj/mutant). Run `rake mutant` and look for percentage coverage. In general this should only go up.
|
521
|
+
|
522
|
+
## License
|
523
|
+
|
524
|
+
MIT License (Expat License), see [LICENSE](./LICENSE)
|
525
|
+
|
526
|
+
![](shaved_yak.gif)
|