representors 0.0.5

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