yaks 0.0.0 → 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 +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
@@ -0,0 +1,68 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'virtus'
|
4
|
+
require 'yaks'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
Example = JSON.parse %q<
|
8
|
+
{
|
9
|
+
"posts": [{
|
10
|
+
"id": "1",
|
11
|
+
"title": "Rails is Omakase",
|
12
|
+
"links": {
|
13
|
+
"author": "9",
|
14
|
+
"comments": [ "5", "12", "17", "20" ]
|
15
|
+
}
|
16
|
+
}]
|
17
|
+
}
|
18
|
+
>
|
19
|
+
|
20
|
+
class Author
|
21
|
+
include Virtus.model
|
22
|
+
attribute :id, String
|
23
|
+
end
|
24
|
+
|
25
|
+
class Comment
|
26
|
+
include Virtus.model
|
27
|
+
attribute :id, String
|
28
|
+
end
|
29
|
+
|
30
|
+
class Post
|
31
|
+
include Virtus.model
|
32
|
+
attribute :id, String
|
33
|
+
attribute :title, String
|
34
|
+
attribute :author, Author
|
35
|
+
attribute :comments, Array[Comment]
|
36
|
+
end
|
37
|
+
|
38
|
+
class AuthorMapper < Yaks::Mapper
|
39
|
+
attributes :id
|
40
|
+
end
|
41
|
+
|
42
|
+
class CommentMapper < Yaks::Mapper
|
43
|
+
attributes :id
|
44
|
+
end
|
45
|
+
|
46
|
+
class PostMapper < Yaks::Mapper
|
47
|
+
attributes :id, :title, :links
|
48
|
+
|
49
|
+
has_one :author, mapper: AuthorMapper
|
50
|
+
has_many :comments, mapper: CommentMapper
|
51
|
+
end
|
52
|
+
|
53
|
+
post = Post.new(
|
54
|
+
id: 1,
|
55
|
+
title: "Rails is Omakase",
|
56
|
+
author: Author.new(id: "9"),
|
57
|
+
comments: [5, 12, 17, 20].map {|id| Comment.new(id: id.to_s)}
|
58
|
+
)
|
59
|
+
|
60
|
+
resource = PostMapper.new(post).to_resource
|
61
|
+
json_api = Yaks::JsonApiSerializer.new(resource).to_json_api
|
62
|
+
|
63
|
+
gem 'minitest'
|
64
|
+
require 'minitest/autorun'
|
65
|
+
|
66
|
+
describe('json-api') {
|
67
|
+
specify { assert_equal Example, json_api }
|
68
|
+
}
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'yaks'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
Example = JSON.parse %q<
|
6
|
+
{
|
7
|
+
"posts": [{
|
8
|
+
"id": "1",
|
9
|
+
"title": "Rails is Omakase",
|
10
|
+
"links": {
|
11
|
+
"author": "9"
|
12
|
+
}
|
13
|
+
}],
|
14
|
+
"linked": {
|
15
|
+
"people": [{
|
16
|
+
"id": "9",
|
17
|
+
"name": "@d2h"
|
18
|
+
}]
|
19
|
+
}
|
20
|
+
}
|
21
|
+
>
|
22
|
+
|
23
|
+
|
24
|
+
class Person
|
25
|
+
include Virtus.model
|
26
|
+
attribute :id, String
|
27
|
+
attribute :name, String
|
28
|
+
end
|
29
|
+
|
30
|
+
class Post
|
31
|
+
include Virtus.model
|
32
|
+
attribute :id, String
|
33
|
+
attribute :title, String
|
34
|
+
attribute :author, Person
|
35
|
+
end
|
36
|
+
|
37
|
+
class PersonMapper < Yaks::Mapper
|
38
|
+
attributes :id, :name
|
39
|
+
end
|
40
|
+
|
41
|
+
class PostMapper < Yaks::Mapper
|
42
|
+
attributes :id, :title, :links
|
43
|
+
|
44
|
+
has_one :author, mapper: PersonMapper
|
45
|
+
end
|
46
|
+
|
47
|
+
post = Post.new(
|
48
|
+
id: 1,
|
49
|
+
title: "Rails is Omakase",
|
50
|
+
author: Person.new(id: "9", name: "@d2h"),
|
51
|
+
)
|
52
|
+
|
53
|
+
resource = PostMapper.new(post).to_resource
|
54
|
+
|
55
|
+
json_api = Yaks::JsonApiSerializer.new(resource, embed: :resources).to_json_api
|
56
|
+
|
57
|
+
gem 'minitest'
|
58
|
+
require 'minitest/autorun'
|
59
|
+
|
60
|
+
describe('json-api') {
|
61
|
+
specify { assert_equal Example, json_api }
|
62
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'yaks'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
Example = JSON.parse %q<
|
6
|
+
{
|
7
|
+
"posts": [{
|
8
|
+
"id": "1",
|
9
|
+
"title": "Rails is Omakase",
|
10
|
+
"links": {
|
11
|
+
"author": "http://example.com/people/1",
|
12
|
+
"comments": "http://example.com/comments/5,12,17,20"
|
13
|
+
}
|
14
|
+
}]
|
15
|
+
}
|
16
|
+
>
|
17
|
+
|
18
|
+
|
19
|
+
class Person
|
20
|
+
include Virtus.model
|
21
|
+
attribute :id, String
|
22
|
+
attribute :name, String
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
class Comment
|
27
|
+
include Virtus.model
|
28
|
+
attribute :id, String
|
29
|
+
end
|
30
|
+
|
31
|
+
class Post
|
32
|
+
include Virtus.model
|
33
|
+
attribute :id, String
|
34
|
+
attribute :title, String
|
35
|
+
attribute :author, Person
|
36
|
+
attribute :comments, Array[Comment]
|
37
|
+
end
|
38
|
+
|
39
|
+
class BaseMapper < Yaks::Mapper
|
40
|
+
link :self, 'http://example.com/{plural_profile_type}/{id}'
|
41
|
+
|
42
|
+
def plural_profile_type
|
43
|
+
pluralize(profile_type.to_s)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class CollectionMapper < Yaks::CollectionMapper
|
48
|
+
link :self, 'http://example.com/{plural_profile_type}/{id*}'
|
49
|
+
|
50
|
+
def plural_profile_type
|
51
|
+
pluralize(profile_type.to_s)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class CommentMapper < BaseMapper
|
56
|
+
attributes :id
|
57
|
+
end
|
58
|
+
|
59
|
+
class PersonMapper < BaseMapper
|
60
|
+
attributes :id, :name
|
61
|
+
end
|
62
|
+
|
63
|
+
class PostMapper < BaseMapper
|
64
|
+
attributes :id, :title
|
65
|
+
|
66
|
+
has_one :author, mapper: PersonMapper
|
67
|
+
has_many :comments, mapper: CommentMapper, collection_mapper: CollectionMapper
|
68
|
+
end
|
69
|
+
|
70
|
+
post = Post.new(
|
71
|
+
id: 1,
|
72
|
+
title: "Rails is Omakase",
|
73
|
+
author: Person.new(id: "1", name: "@d2h"),
|
74
|
+
comments: [5, 12, 17, 20].map {|id| Comment.new(id: id.to_s)}
|
75
|
+
)
|
76
|
+
|
77
|
+
resource = PostMapper.new(post).to_resource
|
78
|
+
|
79
|
+
json_api = Yaks::JsonApiSerializer.new(resource, embed: :links).to_json_api
|
80
|
+
|
81
|
+
gem 'minitest'
|
82
|
+
require 'minitest/autorun'
|
83
|
+
|
84
|
+
describe('json-api') {
|
85
|
+
specify { assert_equal Example, json_api }
|
86
|
+
}
|
data/lib/yaks.rb
CHANGED
@@ -1,43 +1,73 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
|
3
5
|
require 'hamster'
|
4
6
|
require 'concord'
|
5
7
|
require 'inflection'
|
8
|
+
require 'uri_template'
|
9
|
+
|
10
|
+
require 'yaks/util'
|
11
|
+
require 'yaks/link_lookup'
|
12
|
+
require 'yaks/shared_options'
|
13
|
+
require 'yaks/primitivize'
|
14
|
+
|
15
|
+
require 'yaks/profile_registry'
|
16
|
+
require 'yaks/rel_registry'
|
17
|
+
require 'yaks/default_policy'
|
6
18
|
|
7
19
|
module Yaks
|
8
20
|
Undefined = Object.new
|
9
21
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
22
|
+
YAKS_DEFAULT_OPTIONS = {
|
23
|
+
policy: DefaultPolicy.new,
|
24
|
+
profile_registry: NullProfileRegistry.new,
|
25
|
+
rel_registry: NullRelRegistry.new,
|
26
|
+
singular_links: [:self, :profile]
|
27
|
+
}
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def Hash(object = nil)
|
31
|
+
return object if object.is_a? Hamster::Hash
|
32
|
+
return Hamster::EmptyHash if object.nil?
|
33
|
+
Hamster.hash(object)
|
34
|
+
end
|
35
|
+
|
36
|
+
def List(*entries)
|
37
|
+
case entries.size
|
38
|
+
when 0
|
39
|
+
Hamster::EmptyList
|
40
|
+
when 1
|
41
|
+
if entries.first.respond_to? :to_list
|
42
|
+
entries.first.to_list
|
43
|
+
else
|
44
|
+
Hamster.list(*entries.compact)
|
45
|
+
end
|
15
46
|
else
|
16
|
-
|
47
|
+
Hamster.list(*entries)
|
17
48
|
end
|
18
49
|
end
|
19
50
|
end
|
51
|
+
extend ClassMethods
|
20
52
|
|
21
|
-
|
22
|
-
Hamster.list(*args)
|
23
|
-
end
|
53
|
+
end
|
24
54
|
|
25
|
-
def Hash(*args)
|
26
|
-
Hamster.hash(*args)
|
27
|
-
end
|
28
55
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
56
|
+
require 'yaks/resource'
|
57
|
+
require 'yaks/null_resource'
|
58
|
+
require 'yaks/resource/link'
|
59
|
+
require 'yaks/collection_resource'
|
60
|
+
|
61
|
+
require 'yaks/mapper/link'
|
62
|
+
require 'yaks/mapper/association'
|
63
|
+
require 'yaks/mapper/has_one'
|
64
|
+
require 'yaks/mapper/has_many'
|
65
|
+
require 'yaks/mapper/config'
|
66
|
+
require 'yaks/mapper/class_methods'
|
67
|
+
require 'yaks/mapper/map_links'
|
68
|
+
require 'yaks/mapper'
|
69
|
+
require 'yaks/collection_mapper'
|
33
70
|
|
34
|
-
require 'yaks/util'
|
35
|
-
require 'yaks/serializable_collection'
|
36
|
-
require 'yaks/serializable_object'
|
37
|
-
require 'yaks/serializable_association'
|
38
|
-
require 'yaks/fold_json_api'
|
39
|
-
require 'yaks/fold_ams_compat'
|
40
|
-
require 'yaks/serializer/class_methods'
|
41
71
|
require 'yaks/serializer'
|
42
|
-
require 'yaks/
|
43
|
-
require 'yaks/
|
72
|
+
require 'yaks/hal_serializer'
|
73
|
+
require 'yaks/json_api_serializer'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Yaks
|
4
|
+
class CollectionMapper
|
5
|
+
include Util, Mapper::MapLinks, SharedOptions
|
6
|
+
extend Mapper::ClassMethods
|
7
|
+
|
8
|
+
attr_reader :collection, :resource_mapper, :options
|
9
|
+
private :collection, :resource_mapper, :options
|
10
|
+
|
11
|
+
def_delegators 'self.class', :config
|
12
|
+
def_delegators :config, :links
|
13
|
+
|
14
|
+
def initialize(collection, resource_mapper, options = {})
|
15
|
+
@collection = collection
|
16
|
+
@resource_mapper = resource_mapper
|
17
|
+
@options = YAKS_DEFAULT_OPTIONS.merge(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_resource
|
21
|
+
CollectionResource.new(map_links, collection.map {|obj| resource_mapper.new(obj, options).to_resource})
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_attribute(name)
|
25
|
+
respond_to?(name) ? send(name) : collection.map(&name.to_sym)
|
26
|
+
end
|
27
|
+
|
28
|
+
def profile_type
|
29
|
+
resource_mapper.new(nil, options).profile_type
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Yaks
|
2
|
+
# A collection of Resource objects, it has members, and its own set of link
|
3
|
+
# relations like self and profile describing the collection.
|
4
|
+
#
|
5
|
+
# A collection can be the top-level result of an API call, like all posts to
|
6
|
+
# a blog, or a subresource collection, like the comments on a post result.
|
7
|
+
#
|
8
|
+
# Some formats treat everything like a collection, and a single resource as a
|
9
|
+
# collection of one. Others treat every top level response as singular, e.g.
|
10
|
+
# a single "collection of orders". Because of this Resource and
|
11
|
+
# CollectionResource can both be iterated with #each, for the "everything is
|
12
|
+
# a collection crowd", and they both respond to `links`, `attributes` and
|
13
|
+
# `subresources`, so they can both be approached like a singular resource.
|
14
|
+
#
|
15
|
+
# In the second case a collection has a single "subresource", being its
|
16
|
+
# members.
|
17
|
+
class CollectionResource
|
18
|
+
include Equalizer.new(:links, :members)
|
19
|
+
include Enumerable, LinkLookup
|
20
|
+
|
21
|
+
extend Forwardable
|
22
|
+
|
23
|
+
attr_reader :links, :members
|
24
|
+
|
25
|
+
def_delegators :members, :each
|
26
|
+
|
27
|
+
def initialize(links, members)
|
28
|
+
@links = Yaks::List(links)
|
29
|
+
@members = Yaks::List(members)
|
30
|
+
end
|
31
|
+
|
32
|
+
def attributes
|
33
|
+
Yaks::Hash()
|
34
|
+
end
|
35
|
+
|
36
|
+
# Make a CollectionResource quack like a resource.
|
37
|
+
#
|
38
|
+
# At the moment this is only for HAL, which always assumes
|
39
|
+
# a singular resource at the top level, this way it can treat
|
40
|
+
# whatever it gets as a single resource with links and subresources,
|
41
|
+
# we just push the collection down one level.
|
42
|
+
#
|
43
|
+
# Once inside subresources the HAL serializer does check if a resource
|
44
|
+
# is a collection, since there it does make a distinction, and because
|
45
|
+
# in that case it will iterate with each/map rather than calling subresources,
|
46
|
+
# this doesn't cause infinite recursion. Not very pretty, needs looking at.
|
47
|
+
#
|
48
|
+
# :(
|
49
|
+
def subresources
|
50
|
+
if members && members.any?
|
51
|
+
Yaks::Hash( profile => self )
|
52
|
+
else
|
53
|
+
Yaks::Hash()
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def []
|
58
|
+
end
|
59
|
+
|
60
|
+
def collection?
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Yaks
|
2
|
+
class DefaultPolicy
|
3
|
+
include Util
|
4
|
+
|
5
|
+
def derive_profile_from_mapper(mapper)
|
6
|
+
underscore(mapper.class.name.sub(/Mapper$/, '')).to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
def derive_missing_mapper_for_association(association)
|
10
|
+
Object.const_get("#{camelize(association.name.to_s)}Mapper")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
# application/hal+json
|
4
|
+
#
|
5
|
+
# see examples/hal01.rb
|
6
|
+
|
7
|
+
module Yaks
|
8
|
+
class HalSerializer < Serializer
|
9
|
+
|
10
|
+
def serialize
|
11
|
+
Primitivize.(serialize_resource(resource))
|
12
|
+
end
|
13
|
+
alias to_hal serialize
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def serialize_resource(resource)
|
18
|
+
# The HAL spec doesn't say explicitly how to deal missing values,
|
19
|
+
# looking at client behavior (Hyperagent) it seems safer to return an empty
|
20
|
+
# resource.
|
21
|
+
#
|
22
|
+
# return nil if resource.is_a? NullResource
|
23
|
+
result = resource.attributes
|
24
|
+
result = result.put(:_links, serialize_links(resource.links)) unless resource.links.empty?
|
25
|
+
result = result.put(:_embedded, serialize_embedded(resource.subresources)) unless resource.subresources.empty?
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialize_links(links)
|
30
|
+
links.reduce(Yaks::Hash(), &method(:serialize_link))
|
31
|
+
end
|
32
|
+
|
33
|
+
def serialize_link(memo, link)
|
34
|
+
memo.put(link.rel) {|links|
|
35
|
+
slink = {href: link.uri}.merge(link.options.reject{|k,_| k==:templated})
|
36
|
+
slink.merge!(templated: true) if link.templated?
|
37
|
+
singular?(link.rel) ? slink : Yaks::List(links).cons(slink)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def singular?(rel)
|
42
|
+
options.fetch(:singular_links) { [] }.include?(rel)
|
43
|
+
end
|
44
|
+
|
45
|
+
def serialize_embedded(subresources)
|
46
|
+
subresources.map do |rel, resources|
|
47
|
+
[
|
48
|
+
rel,
|
49
|
+
if resources.collection?
|
50
|
+
resources.map( &method(:serialize_resource) )
|
51
|
+
else
|
52
|
+
serialize_resource(resources)
|
53
|
+
end
|
54
|
+
]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|