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