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