yaks 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +13 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +23 -0
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +32 -6
  6. data/README.md +182 -80
  7. data/examples/hal01.rb +126 -0
  8. data/examples/jsonapi01.rb +68 -0
  9. data/examples/jsonapi02.rb +62 -0
  10. data/examples/jsonapi03.rb +86 -0
  11. data/lib/yaks.rb +55 -25
  12. data/lib/yaks/collection_mapper.rb +33 -0
  13. data/lib/yaks/collection_resource.rb +65 -0
  14. data/lib/yaks/default_policy.rb +13 -0
  15. data/lib/yaks/hal_serializer.rb +59 -0
  16. data/lib/yaks/json_api_serializer.rb +59 -0
  17. data/lib/yaks/link_lookup.rb +23 -0
  18. data/lib/yaks/mapper.rb +59 -0
  19. data/lib/yaks/mapper/association.rb +43 -0
  20. data/lib/yaks/mapper/class_methods.rb +36 -0
  21. data/lib/yaks/mapper/config.rb +79 -0
  22. data/lib/yaks/mapper/has_many.rb +15 -0
  23. data/lib/yaks/mapper/has_one.rb +10 -0
  24. data/lib/yaks/mapper/link.rb +74 -0
  25. data/lib/yaks/mapper/lookup.rb +19 -0
  26. data/lib/yaks/mapper/map_links.rb +17 -0
  27. data/lib/yaks/null_resource.rb +28 -0
  28. data/lib/yaks/primitivize.rb +34 -15
  29. data/lib/yaks/profile_registry.rb +60 -0
  30. data/lib/yaks/rel_registry.rb +20 -0
  31. data/lib/yaks/resource.rb +28 -0
  32. data/lib/yaks/resource/link.rb +21 -0
  33. data/lib/yaks/serializer.rb +11 -53
  34. data/lib/yaks/shared_options.rb +15 -0
  35. data/lib/yaks/util.rb +76 -5
  36. data/lib/yaks/version.rb +1 -1
  37. data/spec/integration/map_to_resource_spec.rb +30 -0
  38. data/spec/json/hal_plant_collection.json +34 -0
  39. data/spec/spec_helper.rb +10 -1
  40. data/spec/support/friends_mapper.rb +29 -0
  41. data/spec/support/pet_mapper.rb +5 -0
  42. data/spec/support/pet_peeve_mapper.rb +3 -0
  43. data/spec/support/serializers.rb +11 -11
  44. data/spec/support/shared_contexts.rb +47 -0
  45. data/spec/support/shorthands.rb +22 -0
  46. data/spec/yaks/collection_resource_spec.rb +9 -0
  47. data/spec/yaks/hal_serializer_spec.rb +9 -0
  48. data/spec/yaks/mapper/association_spec.rb +21 -0
  49. data/spec/yaks/mapper/class_methods_spec.rb +28 -0
  50. data/spec/yaks/mapper/config_spec.rb +77 -0
  51. data/spec/yaks/mapper/has_one_spec.rb +16 -0
  52. data/spec/yaks/mapper/link_spec.rb +42 -0
  53. data/spec/yaks/mapper/map_links_spec.rb +46 -0
  54. data/spec/yaks/mapper_spec.rb +47 -0
  55. data/spec/yaks/resource_spec.rb +23 -0
  56. data/yaks.gemspec +6 -3
  57. metadata +115 -27
  58. data/lib/yaks/dumper.rb +0 -23
  59. data/lib/yaks/fold_ams_compat.rb +0 -33
  60. data/lib/yaks/fold_json_api.rb +0 -61
  61. data/lib/yaks/serializable_association.rb +0 -21
  62. data/lib/yaks/serializable_collection.rb +0 -10
  63. data/lib/yaks/serializable_object.rb +0 -18
  64. data/lib/yaks/serializer/class_methods.rb +0 -76
  65. data/spec/integration_spec.rb +0 -57
  66. data/spec/yaks/json_api_folder_spec.rb +0 -63
  67. 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
- class << self
11
- def default_serializer_lookup(obj = Undefined)
12
- return method(:default_serializer_lookup) if obj == Undefined
13
- if obj.respond_to?(:to_str)
14
- Object.const_get("#{Util.singular(Util.camelize(obj.to_str))}Serializer")
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
- Object.const_get("#{obj.class.name}Serializer")
47
+ Hamster.list(*entries)
17
48
  end
18
49
  end
19
50
  end
51
+ extend ClassMethods
20
52
 
21
- def List(*args)
22
- Hamster.list(*args)
23
- end
53
+ end
24
54
 
25
- def Hash(*args)
26
- Hamster.hash(*args)
27
- end
28
55
 
29
- def Set(*args)
30
- Hamster.set(*args)
31
- end
32
- end
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/primitivize'
43
- require 'yaks/dumper'
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