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,34 @@
1
+ {
2
+ "_links" : {
3
+ "profile" : { "href" : "http://api.example.com/doc/plant_collection" },
4
+ "self" : { "href" : "http://api.example.com/plants" }
5
+ },
6
+ "_embedded" : {
7
+ "http://api.example.com/doc/plant_collection" : [
8
+ {
9
+ "_links" : {
10
+ "profile" : { "href" : "http://api.example.com/doc/plant" },
11
+ "self" : { "href" : "http://api.example.com/plants/7/plain_grass" }
12
+ },
13
+ "name" : "Plain grass",
14
+ "type" : "grass"
15
+ },
16
+ {
17
+ "_links" : {
18
+ "profile" : { "href" : "http://api.example.com/doc/plant" },
19
+ "self" : { "href" : "http://api.example.com/plants/15/oak" }
20
+ },
21
+ "name" : "Oak",
22
+ "type" : "tree"
23
+ },
24
+ {
25
+ "_links" : {
26
+ "profile" : { "href" : "http://api.example.com/doc/plant" },
27
+ "self" : { "href" : "http://api.example.com/plants/33/passiflora" }
28
+ },
29
+ "name" : "Passiflora",
30
+ "type" : "flower"
31
+ }
32
+ ]
33
+ }
34
+ }
data/spec/spec_helper.rb CHANGED
@@ -6,7 +6,16 @@ $LOAD_PATH.unshift(ROOT.join('lib'))
6
6
 
7
7
  require 'yaks'
8
8
  require 'virtus'
9
+ require 'json'
9
10
 
10
11
  require_relative 'support/models'
11
- require_relative 'support/serializers'
12
+ require_relative 'support/pet_mapper'
13
+ require_relative 'support/pet_peeve_mapper'
14
+ require_relative 'support/friends_mapper'
12
15
  require_relative 'support/fixtures'
16
+ require_relative 'support/shorthands'
17
+ require_relative 'support/shared_contexts'
18
+
19
+ def load_json_fixture(name)
20
+ JSON.parse(ROOT.join('spec/json', name + '.json').read)
21
+ end
@@ -0,0 +1,29 @@
1
+ class FriendMapper < Yaks::Mapper
2
+ attributes :id, :name
3
+
4
+ link :copyright, '/api/copyright/{year}'
5
+
6
+ def year
7
+ 2024
8
+ end
9
+
10
+ has_one :pet_peeve, mapper: PetPeeveMapper
11
+ has_many :pets, mapper: PetMapper
12
+
13
+ # has_one :production_company, # retrieve as `show.production_company`
14
+ # profile: :company, # use the company profile link from the registry, e.g. 'http://foo.api/profiles/company'
15
+ # as: :producer, # serialize as {"producers" => []} in JSON-API or [{class: ["producer"]}] in Siren
16
+ # embed: :link, # don't embed the whole data, link to it, could also be embed: :resource
17
+ # mapper: CompanyMapper, # use this to find the resource URL, or to map the embedded resource
18
+ # rel: :show_production_company # find the relation in the relation registry, e.g. http://foo.apo/rels/show_production_company
19
+
20
+ # # Full derived defaults
21
+ # has_one :production_company
22
+ # # profile: :production_company, # same as the 'name'
23
+ # # as: :production_company, # same as the 'name'
24
+ # # embed: :links # depends on what is configured as default
25
+ # # mapper: CompanyMapper # found by matching the profile
26
+ # # rel: :show_production_company # "#{context.profile_name}_#{relation.profile_name}"
27
+
28
+ # # has_many :pets
29
+ end
@@ -0,0 +1,5 @@
1
+ class PetMapper < Yaks::Mapper
2
+ attributes :id, :name, :species
3
+
4
+ #link :collection, '/api/pets/{id*}'
5
+ end
@@ -0,0 +1,3 @@
1
+ class PetPeeveMapper < Yaks::Mapper
2
+ attributes :id, :type
3
+ end
@@ -1,14 +1,14 @@
1
- class FriendSerializer < Yaks::Serializer
2
- attributes :id, :name
1
+ # class FriendSerializer < Yaks::Mapper
2
+ # attributes :id, :name
3
3
 
4
- has_many :pets
5
- has_one :pet_peeve
6
- end
4
+ # has_many :pets
5
+ # has_one :pet_peeve
6
+ # end
7
7
 
8
- class PetSerializer < Yaks::Serializer
9
- attributes :id, :name, :species
10
- end
8
+ # class PetSerializer < Yaks::Mapper
9
+ # attributes :id, :name, :species
10
+ # end
11
11
 
12
- class PetPeeveSerializer < Yaks::Serializer
13
- attributes :id, :type
14
- end
12
+ # class PetPeeveSerializer < Yaks::Mapper
13
+ # attributes :id, :type
14
+ # end
@@ -0,0 +1,47 @@
1
+ shared_context 'collection resource' do
2
+ let(:resource) { Yaks::CollectionResource.new(links, members) }
3
+ let(:links) { [] }
4
+ let(:members) { [] }
5
+ end
6
+
7
+ shared_context 'plant collection resource' do
8
+ include_context 'collection resource'
9
+
10
+ let(:links) { [ plants_self_link, plants_profile_link ] }
11
+ let(:members) { [ plain_grass, oak, passiflora ] }
12
+
13
+ [
14
+ [ :plant , :profile , 'http://api.example.com/doc/plant' ],
15
+ [ :plants , :profile , 'http://api.example.com/doc/plant_collection' ],
16
+ [ :plants , :self , 'http://api.example.com/plants' ],
17
+ [ :plain_grass , :self , 'http://api.example.com/plants/7/plain_grass' ],
18
+ [ :oak , :self , 'http://api.example.com/plants/15/oak' ],
19
+ [ :passiflora , :self , 'http://api.example.com/plants/33/passiflora' ],
20
+ ].each do |name, type, uri|
21
+ let(:"#{name}_#{type}_link") { Yaks::Resource::Link.new(type, uri, {}) }
22
+ end
23
+
24
+ let(:plain_grass) do
25
+ Yaks::Resource.new(
26
+ {name: "Plain grass", type: "grass"},
27
+ [plain_grass_self_link, plant_profile_link],
28
+ {}
29
+ )
30
+ end
31
+
32
+ let(:oak) do
33
+ Yaks::Resource.new(
34
+ {name: "Oak", type: "tree"},
35
+ [oak_self_link, plant_profile_link],
36
+ {}
37
+ )
38
+ end
39
+
40
+ let(:passiflora) do
41
+ Yaks::Resource.new(
42
+ {name: "Passiflora", type: "flower"},
43
+ [passiflora_self_link, plant_profile_link],
44
+ {}
45
+ )
46
+ end
47
+ end
@@ -0,0 +1,22 @@
1
+ shared_context 'shorthands' do
2
+ let(:resource) {
3
+ ->(attrs, links = nil) {
4
+ Yaks::Resource.new(Yaks::Hash(attrs), links, nil)
5
+ }
6
+ }
7
+
8
+ let(:collection_resource) {
9
+ ->(*attr_hashes) {
10
+ Yaks::CollectionResource.new(
11
+ nil,
12
+ Yaks::List(attr_hashes.map(&resource.method(:call)))
13
+ )
14
+ }
15
+ }
16
+
17
+ let(:resource_link) {
18
+ ->(rel, uri, options = {}) {
19
+ Yaks::Resource::Link.new(rel, uri, options)
20
+ }
21
+ }
22
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::CollectionResource do
4
+ it 'should normalize its arguments' do
5
+ expect(described_class.new(nil, nil)).to eq(
6
+ described_class.new(Yaks::List(), Yaks::List())
7
+ )
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::HalSerializer do
4
+ include_context 'plant collection resource'
5
+
6
+ subject { described_class.new(resource).serialize }
7
+
8
+ it { should eq(load_json_fixture('hal_plant_collection')) }
9
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Mapper::Association do
4
+ describe 'self_link' do
5
+ context 'without a :self link' do
6
+ subject { described_class.new(nil, nil, nil, Yaks::List(Yaks::Mapper::Link.new(:alternate, '/foo')), {}) }
7
+
8
+ it 'should be nil' do
9
+ expect(subject.self_link).to be_nil
10
+ end
11
+ end
12
+ end
13
+
14
+ context 'with a self link' do
15
+ subject { described_class.new(nil, nil, nil, Yaks::List(Yaks::Mapper::Link.new(:self, '/self')), {}) }
16
+
17
+ it 'should resolve to the self link' do
18
+ expect(subject.self_link).to eq Yaks::Mapper::Link.new(:self, '/self')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Mapper::ClassMethods do
4
+ subject { Class.new { extend Yaks::Mapper::ClassMethods } }
5
+
6
+ describe 'attributes' do
7
+ before do
8
+ subject.attributes(:foo, :bar)
9
+ end
10
+
11
+ it 'should allow setting them' do
12
+ expect( subject.attributes ).to eq Hamster.list(:foo, :bar)
13
+ end
14
+
15
+ describe 'with inheritance' do
16
+ let(:child) { Class.new(subject) }
17
+ before { child.attributes(:baz) }
18
+
19
+ it 'should inherit attributes from the parent' do
20
+ expect(child.attributes).to eq Hamster.list(:foo, :bar, :baz)
21
+ end
22
+
23
+ it 'should not alter the parent' do
24
+ expect(subject.attributes).to eq Hamster.list(:foo, :bar)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Mapper::Config do
4
+ subject(:config) { described_class.new }
5
+
6
+ describe 'attributes' do
7
+ context 'an empty config' do
8
+ it 'should return an empty attributes list' do
9
+ expect(config.attributes).to eq Yaks::List()
10
+ end
11
+ end
12
+
13
+ it 'should add attributes' do
14
+ expect(config.attributes(:foo, :bar, :baz).attributes)
15
+ .to eq Hamster.list(:foo, :bar, :baz)
16
+ end
17
+
18
+ it 'should be chainable' do
19
+ expect(
20
+ config
21
+ .attributes(:foo, :bar)
22
+ .attributes(:baz)
23
+ .attributes
24
+ ).to eq Hamster.list(:foo, :bar, :baz)
25
+ end
26
+ end
27
+
28
+ describe 'links' do
29
+ context 'an empty config' do
30
+ it 'should have an empty link list' do
31
+ expect(config.links).to eq Yaks::List()
32
+ end
33
+ end
34
+
35
+ describe 'adding a link' do
36
+ let(:config) { subject.link(:self, '/foo/bar/{id}') }
37
+
38
+ it 'should have it in the link list' do
39
+ expect(config.links).to eq Yaks::List(Yaks::Mapper::Link.new(:self, '/foo/bar/{id}'))
40
+ end
41
+ end
42
+
43
+ describe 'setting a profile' do
44
+ let(:config) { subject.profile :post }
45
+
46
+ it 'should set the profile name' do
47
+ expect(config.profile).to eq :post
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'associations' do
53
+ describe 'has_one' do
54
+ let(:config) { subject.has_one :mother, mapper: Yaks::Mapper }
55
+
56
+ it 'should have the association configured' do
57
+ expect(config.associations).to eq Yaks::List(Yaks::Mapper::HasOne.new(:mother, :mother, Yaks::Mapper, Yaks::List(), {}))
58
+ end
59
+ end
60
+
61
+ describe 'has_many' do
62
+ let(:config) { subject.has_many :shoes, mapper: Yaks::Mapper }
63
+
64
+ it 'should have the association configured' do
65
+ expect(config.associations).to eq Yaks::List(Yaks::Mapper::HasMany.new(:shoes, :shoes, Yaks::Mapper, Yaks::List(), {}))
66
+ end
67
+ end
68
+
69
+ context 'with an :as alternate key' do
70
+ let(:config) { subject.has_many :shoes, as: :footwear, mapper: Yaks::Mapper }
71
+
72
+ it 'should have the given value as its "key"' do
73
+ expect(config.associations.first.key).to eq :footwear
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Mapper::HasOne do
4
+ include_context 'shorthands'
5
+
6
+ AuthorMapper = Class.new(Yaks::Mapper) { attributes :name }
7
+
8
+ let(:name) { 'William S. Burroughs' }
9
+ let(:mapper) { AuthorMapper }
10
+ let(:has_one) { described_class.new(:author, :author, mapper, [], {}) }
11
+ let(:author) { Struct.new(:name).new(name) }
12
+
13
+ it 'should map to a single Resource' do
14
+ expect(has_one.map_resource(author, {})).to eq resource[{name: name}, [resource_link[:profile, 'author']]]
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Mapper::Link do
4
+ include_context 'shorthands'
5
+
6
+ subject(:link) { described_class.new(rel, template, options) }
7
+
8
+ let(:rel) { :next }
9
+ let(:template) { '/foo/bar/{x}/{y}' }
10
+ let(:options) { {} }
11
+ its(:variables) { should eq ['x', 'y'] }
12
+ its(:uri_template) { should eq URITemplate.new(template) }
13
+
14
+ it 'should expand the template' do
15
+ expect(link.expand(:x => 3, :y => 'foo')).to eq '/foo/bar/3/foo'
16
+ end
17
+
18
+ describe 'expand_with' do
19
+ it 'should look up expansion values through the provided callable' do
20
+ expect(link.expand_with(->(var){ var.upcase })).to eq '/foo/bar/X/Y'
21
+ end
22
+
23
+ context 'with expansion turned off' do
24
+ let(:options) { {expand: false} }
25
+
26
+ it 'should keep the template in the response' do
27
+ expect(link.expand_with(->{ })).to eq '/foo/bar/{x}/{y}'
28
+ end
29
+
30
+ its(:expand?) { should be_false }
31
+ end
32
+
33
+ context 'with a URI without expansion variables' do
34
+ let(:template) { '/orders' }
35
+
36
+ it 'should return the link as is' do
37
+ expect(link.expand_with(->{ })).to eq '/orders'
38
+ end
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Mapper::MapLinks do
4
+ let(:policy) { Class.new(Yaks::DefaultPolicy) { def derive_profile_from_mapper(*); :the_profile ; end }.new }
5
+ let(:mapper_class) do
6
+ Class.new(Yaks::Mapper) do
7
+ def get_title
8
+ "The Title"
9
+ end
10
+ end
11
+ end
12
+
13
+ subject(:mapper) do
14
+ mapper_class.new(Object.new, policy: policy)
15
+ end
16
+
17
+ context 'with a title proc' do
18
+ before do
19
+ mapper_class.link :the_link_rel, '/foo/bar', title: ->{ get_title }
20
+ end
21
+
22
+ it 'should resolve the title' do
23
+ expect(mapper.map_links).to include Yaks::Resource::Link.new(:the_link_rel, '/foo/bar', title: 'The Title')
24
+ end
25
+ end
26
+
27
+ context 'with a title string' do
28
+ before do
29
+ mapper_class.link :the_link_rel, '/foo/bar', title: 'fixed string'
30
+ end
31
+
32
+ it 'should resolve the title' do
33
+ expect(mapper.map_links).to include Yaks::Resource::Link.new(:the_link_rel, '/foo/bar', title: 'fixed string')
34
+ end
35
+ end
36
+
37
+ context 'with no title set' do
38
+ before do
39
+ mapper_class.link :the_link_rel, '/foo/bar'
40
+ end
41
+
42
+ it 'should resolve the title' do
43
+ expect(mapper.map_links).to include Yaks::Resource::Link.new(:the_link_rel, '/foo/bar', {})
44
+ end
45
+ end
46
+ end