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