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,59 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Yaks
4
+ class JsonApiSerializer < Serializer
5
+ def serialize
6
+ serialized = Yaks::Hash(
7
+ pluralize(profile_name.to_s) => resource.map(&method(:serialize_resource))
8
+ )
9
+
10
+ if options[:embed] == :resources
11
+ linked = resource.reduce(Yaks::Hash()) do |memo, res|
12
+ serialize_linked_subresources(res.subresources, memo)
13
+ end
14
+ serialized = serialized.put('linked', linked)
15
+ end
16
+
17
+ Primitivize.( serialized )
18
+ end
19
+ alias to_json_api serialize
20
+
21
+ def serialize_resource(resource)
22
+ result = resource.attributes
23
+ result = result.merge(Yaks::Hash(:links => serialize_links(resource.subresources))) unless resource.subresources.empty?
24
+ result
25
+ end
26
+
27
+ def serialize_links(subresources)
28
+ Yaks::Hash(subresources.map &method(:serialize_link))
29
+ end
30
+
31
+ def serialize_link(name, resource)
32
+ if options[:embed] == :links
33
+ [ name, resource.uri ]
34
+ else
35
+ [ name, resource.collection? ? resource.map(&curry_symbol(:[], :id)) : resource[:id] ]
36
+ end
37
+ end
38
+
39
+ def serialize_linked_subresources(subresources, linked)
40
+ subresources.reduce(linked) do |memo, name, resources|
41
+ serialize_linked_resources(resources, memo)
42
+ end
43
+ end
44
+
45
+ def serialize_linked_resources(resources, linked)
46
+ resources.reduce(linked) do |memo, resource|
47
+ serialize_subresource(resource, memo)
48
+ end
49
+ end
50
+
51
+ # {shows => [{id: 3, name: 'foo'}]}
52
+ def serialize_subresource(resource, linked)
53
+ key = pluralize(profile_registry.find_by_uri(resource.profile).to_s)
54
+ set = linked.fetch(key) { Hamster.set }
55
+ linked = linked.put(key, set << serialize_resource(resource))
56
+ serialize_linked_subresources(resource.subresources, linked)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ module Yaks
2
+ module LinkLookup
3
+
4
+ def uri
5
+ self_link = links_by_rel(:self).first
6
+ self_link.uri if self_link
7
+ end
8
+
9
+ def profile
10
+ link = links_by_rel(:profile).first
11
+ link.uri if link
12
+ end
13
+
14
+ def profile_type
15
+ profile_registry.find_by_uri(profile)
16
+ end
17
+
18
+ def links_by_rel(rel)
19
+ links.select {|link| link.rel == rel}
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Yaks
4
+ class Mapper
5
+ extend ClassMethods, Forwardable
6
+ include Util, MapLinks, SharedOptions
7
+
8
+ def_delegators 'self.class', :config
9
+ def_delegators :config, :attributes, :links, :associations
10
+
11
+ attr_reader :subject, :options
12
+ private :subject, :options
13
+ alias object subject
14
+
15
+ def initialize(subject, options = {})
16
+ @subject = subject
17
+ @options = YAKS_DEFAULT_OPTIONS.merge(options)
18
+ end
19
+
20
+ def to_resource
21
+ return NullResource.new if subject.nil?
22
+
23
+ Resource.new(
24
+ map_attributes,
25
+ map_links,
26
+ map_subresources
27
+ )
28
+ end
29
+
30
+ def profile_type
31
+ config.profile || policy.derive_profile_from_mapper(self)
32
+ end
33
+
34
+ def map_attributes
35
+ filter(attributes).map &juxt(identity_function, method(:load_attribute))
36
+ end
37
+
38
+ def map_subresources
39
+ filtered = filter(associations.map(&:name))
40
+ associations.select{|assoc| filtered.include? assoc.name}.map do |association|
41
+ association.map_to_resource_pair(profile_type, method(:load_association), options)
42
+ end
43
+ end
44
+
45
+ def load_attribute(name)
46
+ respond_to?(name) ? send(name) : subject.send(name)
47
+ end
48
+ alias load_association load_attribute
49
+
50
+ def profile
51
+ config.profile || policy.derive_missing_profile_from_mapper(self)
52
+ end
53
+
54
+ def filter(attrs)
55
+ attrs
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ module Yaks
2
+ class Mapper
3
+ class Association
4
+ include Equalizer.new(:name, :_mapper, :links)
5
+ include SharedOptions
6
+
7
+ attr_reader :name, :key, :links, :options
8
+ private :links, :options
9
+
10
+ def initialize(name, key, mapper, links, options)
11
+ @name = name
12
+ @key = key
13
+ @mapper = mapper
14
+ @links = links
15
+ @options = options
16
+ end
17
+
18
+ def self_link
19
+ links.detect {|link| link.rel? :self }
20
+ end
21
+
22
+ # @param [Symbol] src_type
23
+ # The profile type of the resource that contains the association
24
+ # @param [Proc] loader
25
+ # A lambda that can retrieve an association by its name
26
+ # @param [Hash] options
27
+ def map_to_resource_pair(src_type, loader, options)
28
+ [ options[:rel_registry].lookup(src_type, key), map_resource(loader.(name), options) ]
29
+ end
30
+
31
+ private
32
+
33
+ def mapper(opts = nil)
34
+ return _mapper unless _mapper == Undefined
35
+ opts[:policy].derive_missing_mapper_for_association(self)
36
+ end
37
+
38
+ def _mapper
39
+ @mapper
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Yaks
4
+ class Mapper
5
+ module ClassMethods
6
+ include Forwardable
7
+ include Util
8
+
9
+ CONFIG_METHODS = [
10
+ :attributes,
11
+ :link,
12
+ :profile,
13
+ # :embed,
14
+ :has_one,
15
+ :has_many
16
+ ]
17
+
18
+ def config
19
+ @config ||= Config.new
20
+ @config = yield(@config) if block_given?
21
+ @config
22
+ end
23
+
24
+ def inherited(child)
25
+ child.config { @config }
26
+ end
27
+
28
+ CONFIG_METHODS.each do |method_name|
29
+ define_method method_name do |*args|
30
+ config &σ(method_name, *args)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,79 @@
1
+ module Yaks
2
+ class Mapper
3
+ class Config
4
+ include Equalizer.new(:attributes)
5
+
6
+ def initialize(attributes = Hamster.list, links = Hamster.list, associations = Hamster.list, profile = nil)
7
+ @attributes = attributes
8
+ @links = links
9
+ @associations = associations
10
+ @profile = profile
11
+ freeze
12
+ end
13
+
14
+ def new(updates)
15
+ self.class.new(
16
+ updates.fetch(:attributes) { attributes },
17
+ updates.fetch(:links) { links },
18
+ updates.fetch(:associations) { associations },
19
+ updates.fetch(:profile) { profile },
20
+ )
21
+ end
22
+
23
+ def attributes(*attrs)
24
+ return @attributes if attrs.empty?
25
+ new(
26
+ attributes: @attributes + attrs.to_list
27
+ )
28
+ end
29
+
30
+ def link(rel, template, options = {})
31
+ new(
32
+ links: @links.cons(Link.new(rel, template, options))
33
+ )
34
+ end
35
+
36
+ def profile(type = Undefined)
37
+ return @profile if type == Undefined
38
+ new(
39
+ profile: type
40
+ )
41
+ end
42
+
43
+ # key
44
+ # embed_style
45
+ # rel
46
+ # (profile)
47
+
48
+ def has_one(name, options = {})
49
+ add_association(HasOne, name, options)
50
+ end
51
+
52
+ def has_many(name, options = {})
53
+ add_association(HasMany, name, options)
54
+ end
55
+
56
+ def add_association(type, name, options = {})
57
+ new(
58
+ associations: @associations.cons(
59
+ type.new(
60
+ name,
61
+ options.fetch(:as) { name },
62
+ options.fetch(:mapper) { Undefined },
63
+ options.fetch(:links) { Yaks::List() },
64
+ options.reject {|k,v| [:as, :mapper, :links].include?(k) }
65
+ )
66
+ )
67
+ )
68
+ end
69
+
70
+ def links
71
+ @links
72
+ end
73
+
74
+ def associations
75
+ @associations
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ module Yaks
2
+ class Mapper
3
+ class HasMany < Association
4
+ def map_resource(collection, opts)
5
+ opts = opts.merge(options)
6
+ collection_mapper(opts).new(collection, mapper(opts), opts).to_resource
7
+ end
8
+
9
+ def collection_mapper(opts)
10
+ opts = opts.merge(options)
11
+ opts.fetch(:collection_mapper) { CollectionMapper }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Yaks
2
+ class Mapper
3
+ class HasOne < Association
4
+ def map_resource(instance, opts)
5
+ opts = opts.merge(options)
6
+ mapper(opts).new(instance, opts).to_resource
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,74 @@
1
+ module Yaks
2
+ class Mapper
3
+ class Link
4
+ include Concord.new(:rel, :template, :options)
5
+ include Util
6
+
7
+ def initialize(rel, template, options = {})
8
+ @rel, @template, @options = rel, template, options
9
+ end
10
+
11
+ def rel?(rel)
12
+ self.rel == rel
13
+ end
14
+
15
+ def expand?
16
+ options.fetch(:expand) {true}
17
+ end
18
+
19
+ def expand_with(callable)
20
+ return template unless expand?
21
+ expand(
22
+ variables.map.with_object({}) do |var, hsh|
23
+ hsh[var] = callable.(var)
24
+ end
25
+ )
26
+ end
27
+
28
+ def map_to_resource_link(mapper)
29
+ make_resource_link(
30
+ rel,
31
+ expand_with(mapper.method(:load_attribute)),
32
+ resource_link_options(mapper)
33
+ )
34
+ end
35
+
36
+ def expand(variables)
37
+ uri_template.expand(variables)
38
+ end
39
+
40
+ def uri_template
41
+ @uri_template ||= URITemplate.new(template)
42
+ end
43
+
44
+ def variables
45
+ uri_template.variables
46
+ end
47
+
48
+ # Link properties defined in HAL
49
+ # href
50
+ # templated
51
+ # typed
52
+ # deprecation
53
+ # name
54
+ # profile
55
+ # title
56
+ # hreflang
57
+
58
+ def resource_link_options(mapper)
59
+ options = self.options
60
+ options = options.merge(title: resolve_title(options[:title], mapper)) if options.has_key?(:title)
61
+ options = options.merge( templated: true ) unless expand?
62
+ options.reject{|k,v| [:expand].include? k}
63
+ end
64
+
65
+ def resolve_title(title_proc, mapper)
66
+ Resolve(title_proc, mapper)
67
+ end
68
+
69
+ def make_resource_link(rel, uri, options)
70
+ Resource::Link.new(rel, uri, options)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,19 @@
1
+ module Yaks
2
+ class Mapper
3
+ module Lookup
4
+
5
+ def serializer_for(object_or_key)
6
+ serializer_class_for(object_or_key).new(object_or_key, options)
7
+ end
8
+
9
+ def serializer_class_for(object_or_key)
10
+ if object_or_key.respond_to?(:to_str) || object_or_key.is_a?(Symbol)
11
+ serializer_lookup.(Object.const_get(Util.singular(Util.camelize(obj.to_s))))
12
+ else
13
+ serializer_lookup.(object_or_key)
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Yaks
4
+ class Mapper
5
+ module MapLinks
6
+
7
+ def map_links
8
+ mapped = links.map &send_with_args(:map_to_resource_link, self)
9
+ unless links.any? {|link| link.rel? :profile }
10
+ mapped = mapped.cons(Resource::Link.new(:profile, profile_registry.find_by_type(profile_type), {}))
11
+ end
12
+ mapped
13
+ end
14
+
15
+ end
16
+ end
17
+ end