representors 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +18 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +126 -0
  5. data/LICENSE.md +19 -0
  6. data/README.md +28 -0
  7. data/Rakefile +10 -0
  8. data/lib/representor_support/utilities.rb +39 -0
  9. data/lib/representors.rb +5 -0
  10. data/lib/representors/errors.rb +7 -0
  11. data/lib/representors/field.rb +108 -0
  12. data/lib/representors/options.rb +67 -0
  13. data/lib/representors/representor.rb +161 -0
  14. data/lib/representors/representor_builder.rb +64 -0
  15. data/lib/representors/representor_hash.rb +59 -0
  16. data/lib/representors/serialization.rb +4 -0
  17. data/lib/representors/serialization/deserializer_base.rb +29 -0
  18. data/lib/representors/serialization/deserializer_factory.rb +13 -0
  19. data/lib/representors/serialization/hal_deserializer.rb +44 -0
  20. data/lib/representors/serialization/hal_serializer.rb +91 -0
  21. data/lib/representors/serialization/hale_deserializer.rb +162 -0
  22. data/lib/representors/serialization/hale_serializer.rb +110 -0
  23. data/lib/representors/serialization/serialization_base.rb +27 -0
  24. data/lib/representors/serialization/serialization_factory_base.rb +54 -0
  25. data/lib/representors/serialization/serializer_base.rb +20 -0
  26. data/lib/representors/serialization/serializer_factory.rb +17 -0
  27. data/lib/representors/transition.rb +130 -0
  28. data/lib/representors/version.rb +4 -0
  29. data/spec/fixtures/complex_hal.json +92 -0
  30. data/spec/fixtures/complex_hale_document.json +81 -0
  31. data/spec/fixtures/drds_hash.rb +120 -0
  32. data/spec/fixtures/hale_spec_examples/basic.json +77 -0
  33. data/spec/fixtures/hale_spec_examples/complex_reference_objects.json +157 -0
  34. data/spec/fixtures/hale_spec_examples/data.json +17 -0
  35. data/spec/fixtures/hale_spec_examples/data_objects.json +96 -0
  36. data/spec/fixtures/hale_spec_examples/link_objects.json +18 -0
  37. data/spec/fixtures/hale_spec_examples/nested_ref.json +43 -0
  38. data/spec/fixtures/hale_spec_examples/reference_objects.json +89 -0
  39. data/spec/fixtures/hale_tutorial_examples/basic_links.json +85 -0
  40. data/spec/fixtures/hale_tutorial_examples/basic_links_with_orders.json +96 -0
  41. data/spec/fixtures/hale_tutorial_examples/basic_links_with_references.json +108 -0
  42. data/spec/fixtures/hale_tutorial_examples/embedded.json +182 -0
  43. data/spec/fixtures/hale_tutorial_examples/empty.json +1 -0
  44. data/spec/fixtures/hale_tutorial_examples/enctype.json +14 -0
  45. data/spec/fixtures/hale_tutorial_examples/final.json +141 -0
  46. data/spec/fixtures/hale_tutorial_examples/get_link.json +17 -0
  47. data/spec/fixtures/hale_tutorial_examples/get_link_with_data.json +29 -0
  48. data/spec/fixtures/hale_tutorial_examples/links.json +11 -0
  49. data/spec/fixtures/hale_tutorial_examples/links_only.json +3 -0
  50. data/spec/fixtures/hale_tutorial_examples/meta.json +208 -0
  51. data/spec/fixtures/hale_tutorial_examples/self_link.json +7 -0
  52. data/spec/fixtures/single_drd.rb +266 -0
  53. data/spec/lib/representors/complex_representor_spec.rb +288 -0
  54. data/spec/lib/representors/field_spec.rb +141 -0
  55. data/spec/lib/representors/representor_builder_spec.rb +223 -0
  56. data/spec/lib/representors/representor_spec.rb +285 -0
  57. data/spec/lib/representors/serialization/deserializer_factory_spec.rb +118 -0
  58. data/spec/lib/representors/serialization/hal_deserializer_spec.rb +34 -0
  59. data/spec/lib/representors/serialization/hal_serializer_spec.rb +171 -0
  60. data/spec/lib/representors/serialization/hale_deserializer_spec.rb +59 -0
  61. data/spec/lib/representors/serialization/hale_roundtrip_spec.rb +34 -0
  62. data/spec/lib/representors/serialization/hale_serializer_spec.rb +659 -0
  63. data/spec/lib/representors/serialization/serializer_factory_spec.rb +108 -0
  64. data/spec/lib/representors/transition_spec.rb +349 -0
  65. data/spec/spec_helper.rb +32 -0
  66. data/spec/support/basic-hale.json +12 -0
  67. data/spec/support/hal_representor_shared.rb +206 -0
  68. data/spec/support/helpers.rb +8 -0
  69. data/tasks/benchmark.rake +75 -0
  70. data/tasks/complex_hal_document.json +98 -0
  71. data/tasks/test_specs.rake +37 -0
  72. data/tasks/yard.rake +22 -0
  73. metadata +232 -0
@@ -0,0 +1,161 @@
1
+ require 'yaml'
2
+ require 'enumerable/lazy' if RUBY_VERSION < '2.0'
3
+ require 'representors/field'
4
+ require 'representors/transition'
5
+ require 'representors/serialization/serializer_factory'
6
+
7
+ module Representors
8
+ ##
9
+ # Manages the respresentation of hypermedia messages for different media-types.
10
+ class Representor
11
+ DEFAULT_PROTOCOL = 'http'
12
+ PROTOCOL_TEMPLATE = "%s://%s"
13
+ UNKNOWN_PROTOCOL = 'ruby_id'
14
+ VALUE_KEY = :value
15
+ META_LINK_FIELDS = ['profile', 'help', 'type', 'self']
16
+
17
+ # @example
18
+ # representor = Representors::Representor.new do |builder|
19
+ # builder.add_attribute({ name: => 'Bob' })
20
+ # end
21
+ #
22
+ # @param [Hash] hash the abstract representor hash defining a resource
23
+ def initialize(hash = {}, builder = nil)
24
+ builder ||= RepresentorBuilder.new(hash)
25
+ builder = yield builder if block_given?
26
+ @representor_hash = builder.to_representor_hash
27
+ end
28
+
29
+ # @param format to convert this representor to
30
+ # @return the representor serialized to a particular media-type like application/hal+json
31
+ def to_media_type(format, options={})
32
+ SerializerFactory.build(format, self).to_media_type(options)
33
+ end
34
+
35
+ def empty?
36
+ @representor_hash.empty?
37
+ end
38
+
39
+ def ==(other)
40
+ other.is_a?(Hash) ? to_hash == other : to_hash == other.to_hash
41
+ end
42
+
43
+ # Returns the document for the representor
44
+ #
45
+ # @return [String] the document for the representor
46
+ def doc
47
+ @doc ||= @representor_hash.doc || ''
48
+ end
49
+
50
+ # The URI for the object
51
+ #
52
+ # @note If the URI can't be made from the provided information it constructs one from the Ruby ID
53
+ # @return [String]
54
+ def identifier
55
+ @identifier ||= begin
56
+ uri = @representor_hash.href || self.object_id
57
+ protocol = @representor_hash.protocol || (uri == self.object_id ? UNKNOWN_PROTOCOL : DEFAULT_PROTOCOL)
58
+ PROTOCOL_TEMPLATE % [protocol, uri]
59
+ end
60
+ end
61
+
62
+ # @return [Hash] The hash representation of the object
63
+ def to_hash
64
+ @to_hash ||= @representor_hash.to_h
65
+ end
66
+
67
+ # @return [String] the yaml representation of the object
68
+ def to_yaml
69
+ @to_yaml ||= YAML.dump(to_hash)
70
+ end
71
+
72
+ # @return [String] so the user can 'puts' this object
73
+ def to_s
74
+ to_hash.inspect
75
+ end
76
+
77
+ # @return [Hash] the resource attributes inferred from representor[:semantics]
78
+ def properties
79
+ @properties ||= Hash[@representor_hash.attributes.map { |k, v| [ k, v[VALUE_KEY]] }]
80
+ end
81
+
82
+ # @return [Enumerable] who's elements are all <Representors:Representor> objects
83
+ def embedded
84
+ @embedded ||= begin
85
+ embedded_representors = @representor_hash.embedded.map do |name, values|
86
+ if values.is_a?(Array)
87
+ several_representors = values.map do |value|
88
+ Representor.new(value)
89
+ end
90
+ [name, several_representors]
91
+ else
92
+ [name, Representor.new(values)]
93
+ end
94
+ end
95
+ Hash[embedded_representors]
96
+ end
97
+ end
98
+
99
+ # @return [Array] who's elements are all <Representors:Transition> objects
100
+ def meta_links
101
+ @meta_links ||= begin
102
+ links_from_transitions = {}
103
+
104
+ transitions.each do |transition|
105
+ if META_LINK_FIELDS.include?(transition.rel)
106
+ links_from_transitions[transition.rel.to_sym] = transition.uri
107
+ end
108
+ end
109
+
110
+ @representor_hash.links.merge(links_from_transitions).map do |k, v|
111
+ Representors::Transition.new({rel: k, href: v})
112
+ end.uniq { |transition| [transition.rel, transition.uri] }
113
+ end
114
+ end
115
+ # @return [Array] who's elements are all <Representors:Transition> objects
116
+ def transitions
117
+ @transitions ||= begin
118
+ transition_hashes = (@representor_hash.transitions + embedded_transitions_hashes).uniq do |hash|
119
+ [hash[:rel], hash[:href]]
120
+ end
121
+ transition_hashes.map { |hash| Transition.new(hash) }
122
+ end
123
+ end
124
+
125
+ # @return [Array] who's elements are all <Representors:Transition> objects from the self links of
126
+ # embedded items, updating the rel to reflect the embedded items key
127
+ def embedded_transitions
128
+ embedded_transitions_hashes.map { |hash| Transition.new(hash) }
129
+ end
130
+
131
+ # @return [Array] who's elements are all <Representors:Option> objects
132
+ def datalists
133
+ @datalists ||= begin
134
+ attributes = transitions.map { |transition| transition.attributes }
135
+ parameters = transitions.map { |transition| transition.parameters }
136
+ fields = [attributes, parameters].flatten
137
+ options = fields.map { |field| field.options }
138
+ options.select { |o| o.datalist? }
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def embedded_transitions_hashes
145
+ @representor_hash.embedded.flat_map do |k,*v|
146
+ v.flatten.map do |item|
147
+ trans_hash = item[:transitions].find { |t| t[:rel] == "self" }
148
+ if trans_hash
149
+ profile_href = item[:links]["profile"] if item[:links]
150
+ trans_hash = trans_hash.merge(profile: profile_href) if profile_href
151
+ trans_hash.merge(rel: k)
152
+ else
153
+ {}
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+
160
+ end
161
+ end
@@ -0,0 +1,64 @@
1
+ require 'representor_support/utilities'
2
+ require 'representors/representor_hash'
3
+
4
+ module Representors
5
+
6
+ ##
7
+ # Builder has methods to abstract the construction of Representor objects
8
+ # In the present implementation it will create a hash of a specific format to
9
+ # Initialize the Representor with, this will create classess with it.
10
+ class RepresentorBuilder
11
+ include RepresentorSupport::Utilities
12
+
13
+ HREF_KEY = :href
14
+ DATA_KEY = :data
15
+
16
+ def initialize(representor_hash = {})
17
+ @representor_hash = RepresentorHash.new(representor_hash)
18
+ end
19
+
20
+ # Returns a hash usable by the representor class
21
+ def to_representor_hash
22
+ @representor_hash
23
+ end
24
+
25
+ # Adds an attribute to the Representor. We are creating a hash where the keys are the
26
+ # names of the attributes
27
+ def add_attribute(name, value, options={})
28
+ new_representor_hash = RepresentorHash.new(deep_dup(@representor_hash.to_h))
29
+ new_representor_hash.attributes[name] = options.merge({value: value})
30
+ RepresentorBuilder.new(new_representor_hash)
31
+ end
32
+
33
+ # Adds a transition to the Representor, each transition is a hash of values
34
+ # The transition collection is an Array
35
+ def add_transition(rel, href, options={})
36
+ new_representor_hash = RepresentorHash.new(deep_dup(@representor_hash.to_h))
37
+ options = symbolize_keys(options)
38
+ options.delete(:method) if options[:method] == Transition::DEFAULT_METHOD
39
+ link_values = options.merge({href: href, rel: rel})
40
+
41
+ if options[DATA_KEY]
42
+ link_values[Transition::DESCRIPTORS_KEY] = link_values.delete(DATA_KEY)
43
+ end
44
+
45
+ new_representor_hash.transitions.push(link_values)
46
+ RepresentorBuilder.new(new_representor_hash)
47
+ end
48
+
49
+ # Adds directly an array to our array of transitions
50
+ def add_transition_array(rel, array_of_hashes)
51
+ array_of_hashes.reduce(RepresentorBuilder.new(@representor_hash)) do |memo, transition|
52
+ transition = symbolize_keys(transition)
53
+ href = transition.delete(:href)
54
+ memo = memo.add_transition(rel, href, transition)
55
+ end
56
+ end
57
+
58
+ def add_embedded(name, embedded_resource)
59
+ new_representor_hash = RepresentorHash.new(deep_dup(@representor_hash.to_h))
60
+ new_representor_hash.embedded[name] = embedded_resource
61
+ RepresentorBuilder.new(new_representor_hash)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,59 @@
1
+ require 'representor_support/utilities'
2
+
3
+ module Representors
4
+ # This is the structure shared between the builder and the representor.
5
+ # This class allows to pass all the data to the representor without polluting it with methods
6
+ # It is supposed to be a fast class (Struct is implemented in C)
7
+ # The structure looks like this:
8
+ # id: [string]
9
+ # doc: [string]
10
+ # href: [string]
11
+ # protocol: [string]
12
+ # attributes: [hash] { key => value }
13
+ # links: [array of hashes]
14
+ # transitions: [array of hashes]
15
+ # embedded: [hash] where each value can be recursively defined by this same structure
16
+ RepresentorHash = Struct.new(:id, :doc, :href, :protocol, :attributes, :embedded, :links, :transitions) do
17
+ include RepresentorSupport::Utilities
18
+
19
+ DEFAULT_HASH_VALUES = {
20
+ id: nil,
21
+ doc: nil,
22
+ href: nil,
23
+ #TODO protocol doesnt belong in representors, remove it
24
+ protocol: nil,
25
+ #TODO fix this, make it the same interface as transitions
26
+ links: {},
27
+ attributes: {},
28
+ transitions: [],
29
+ embedded: {}
30
+ }.each_value(&:freeze).freeze
31
+
32
+
33
+ # be able to create from a hash
34
+ def initialize(hash = {})
35
+ DEFAULT_HASH_VALUES.each { |k, v| self[k] = dup_or_self(hash[k] || v) }
36
+ end
37
+
38
+ # Be able to generate a new structure with myself and a hash
39
+ def merge(hash)
40
+ new_representor_hash = RepresentorHash.new(to_h)
41
+ hash.each_pair { |k, v| new_representor_hash[k] = v }
42
+ new_representor_hash
43
+ end
44
+
45
+ # to_h does not exists in Ruby < 2.0
46
+ def to_h
47
+ members.each_with_object({}) { |member, hash| hash[member] = self[member] }
48
+ end
49
+
50
+ def empty?
51
+ members.all? { |k| self[k].nil? || self[k].empty? }
52
+ end
53
+
54
+ def ==(other)
55
+ members.all? { |k| self[k] == other[k] }
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,4 @@
1
+ require 'representors/serialization/hal_deserializer'
2
+ require 'representors/serialization/hale_deserializer'
3
+ require 'representors/serialization/hal_serializer'
4
+ require 'representors/serialization/hale_serializer'
@@ -0,0 +1,29 @@
1
+ require 'representors/serialization/serialization_base'
2
+ require 'representors/serialization/deserializer_factory'
3
+
4
+ module Representors
5
+ class DeserializerBase < SerializationBase
6
+
7
+ def initialize(target)
8
+ @target = target.empty? ? {} : target
9
+ end
10
+
11
+ def self.inherited(subclass)
12
+ DeserializerFactory.register_deserializers(subclass)
13
+ end
14
+
15
+ # Returns back a class with all the information of the document and with convenience methods
16
+ # to access it.
17
+ # TODO: Yield builder to factor out builder dependency.
18
+ def to_representor
19
+ Representor.new(to_representor_hash)
20
+ end
21
+
22
+ # Returns a hash representation of the data. This is useful to merge with new data which may
23
+ # be built by different builders. In this class we use it to support embedded resources.
24
+ def to_representor_hash(options = {})
25
+ raise "Abstract method #to_representor_hash not implemented in #{self.name} deserializer class."
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ require 'representors/serialization/serialization_factory_base'
2
+
3
+ module Representors
4
+ class DeserializerFactory < SerializationFactoryBase
5
+ def self.register_deserializers(*serializers)
6
+ register_serialization_classes(*serializers)
7
+ end
8
+
9
+ def self.registered_deserializers
10
+ registered_serialization_classes
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+
2
+ require 'representors/serialization/hale_deserializer'
3
+
4
+ module Representors
5
+ ##
6
+ # Deserializes the HAL format as specified in http://stateless.co/hal_specification.html
7
+ # For examples of how this format looks like check the files under spec/fixtures/hal
8
+ # TODO: support Curies http://www.w3.org/TR/2010/NOTE-curie-20101216/
9
+ class HalDeserializer < HaleDeserializer
10
+ media_symbol :hal
11
+ media_type 'application/hal+json', 'application/json'
12
+
13
+ HAL_LINK_KEYS = %w(href templated type deprecation name profile title hreflang)
14
+ @reserved_keys = [LINKS_KEY, EMBEDDED_KEY]
15
+
16
+ private
17
+
18
+ def builder_add_from_deserialized(builder, media)
19
+ builder = deserialize_properties(builder, media)
20
+ builder = deserialize_links(builder, media)
21
+ builder = deserialize_embedded(builder, media)
22
+ end
23
+
24
+ def deserialize_links(builder, media)
25
+ (media[LINKS_KEY] || {}).each do |link_rel, link_values|
26
+ link_values = [link_values] unless link_values.is_a?(Array)
27
+ link_values = link_values.map do |hash|
28
+ hash.select { |k,_| HAL_LINK_KEYS.include?(k) }
29
+ end
30
+ ensure_valid_links!(link_rel, link_values)
31
+ builder = builder.add_transition_array(link_rel, link_values)
32
+ end
33
+ builder
34
+ end
35
+
36
+ def deserialize_properties(builder, media)
37
+ media.each do |k,v|
38
+ builder = builder.add_attribute(k, v) unless (self.class.reserved_keys.include?(k))
39
+ end
40
+ builder
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,91 @@
1
+ require 'representors/serialization/serializer_base'
2
+
3
+ module Representors
4
+ module Serialization
5
+ class HalSerializer < SerializerBase
6
+ LINKS_KEY = "_links"
7
+ EMBEDDED_KEY = "_embedded"
8
+ LINKS_ONLY_OPTION = :embed_links_only
9
+
10
+ media_symbol :hal_json
11
+ media_type 'application/hal+json'
12
+
13
+ # This is public and returning a hash to be able to implement embedded resources
14
+ # serialization
15
+ # TODO: make this private and merge with to_media_type
16
+ # The name is quite misleading,
17
+ def to_hash(options = {})
18
+ base_hash, links, embedded_hals = common_serialization(@target)
19
+ base_hash.merge!(links.merge(embedded_hals.(options)))
20
+ base_hash
21
+ end
22
+
23
+ # This is the main entry of this class. It returns a serialization of the data
24
+ # in a given media type.
25
+ def to_media_type(options = {})
26
+ to_hash.to_json
27
+ end
28
+
29
+ private
30
+
31
+ def common_serialization(representor)
32
+ base_hash = get_semantics(representor)
33
+ embedded_hals = ->(options) { get_embedded_objects(representor, options) }
34
+ # we want to group by rel name because it is possible to have several transitions with the same
35
+ # rel name. This will become an array in the output. For instance an items array
36
+ # with links to each item
37
+ grouped_transitions = (representor.transitions + representor.meta_links).group_by { |transition| transition[:rel] }
38
+ links = build_links(grouped_transitions)
39
+ links = links.empty? ? {} : { LINKS_KEY => links.reduce({}, :merge) }
40
+ [base_hash, links, embedded_hals]
41
+ end
42
+
43
+ def get_semantics(representor)
44
+ representor.properties
45
+ end
46
+
47
+ def get_embedded_objects(representor, options)
48
+ @get_embedded_objects ||= if representor.embedded.empty? || options.has_key?(LINKS_ONLY_OPTION)
49
+ {}
50
+ else
51
+ embedded_elements = representor.embedded.map { |k, v| build_embedded_objects(k, v) }
52
+ { EMBEDDED_KEY => embedded_elements.reduce({}, :merge) }
53
+ end
54
+ end
55
+
56
+ # Lambda used in this case to DRY code. Allows 'is array' functionality to be handled elsewhere
57
+ def build_embedded_objects(key, embedded)
58
+ make_media_type = ->(obj) { self.class.new(obj).to_hash }
59
+ embed = map_or_apply(make_media_type, embedded)
60
+ { key => embed}
61
+ end
62
+
63
+ # @param [Hash] transitions. A hash on the shape "rel_name" => [Transition]
64
+ # The value for the rel_name usually will have only one transition but when we are building an
65
+ # array of transitions will have many.
66
+ # @return [Array] Array of hashes with the format [ { rel_name => {link_info1}}, {rel_name2 => ... }]
67
+ def build_links(transitions)
68
+ transitions.map do |rel_name, transition_array|
69
+ links = transition_array.map{|transition| build_links_for_this_media_type(transition)}
70
+ if links.size > 1
71
+ {rel_name => links}
72
+ else
73
+ {rel_name => links.first}
74
+ end
75
+ end
76
+ end
77
+
78
+ # This method can be overriden by other classes
79
+ # @param transition , a single tansition
80
+ def build_links_for_this_media_type(transition)
81
+ link = if transition.templated?
82
+ { href: transition.templated_uri, templated: true }
83
+ else
84
+ { href: transition.uri }
85
+ end
86
+ [:name, :profile].each { |key| link[key] = transition[key] if transition.has_key?(key) }
87
+ link
88
+ end
89
+ end
90
+ end
91
+ end