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,162 @@
1
+ require 'json'
2
+ require 'representors/serialization/deserializer_base'
3
+ require 'representors/errors'
4
+ require 'representor_support/utilities'
5
+
6
+
7
+ module Representors
8
+ # Class for a Hale document deserializer.
9
+ # Built against Hale version 0.0.1, https://github.com/mdsol/hale/tree/0-0-stable
10
+ # @since 0.0.2
11
+ class HaleDeserializer < DeserializerBase
12
+ media_symbol :hale
13
+ media_type 'application/vnd.hale+json'
14
+
15
+ META_KEY = '_meta'.freeze
16
+ REF_KEY = '_ref'.freeze
17
+ DATA_KEY = 'data'.freeze
18
+ OPTIONS_KEY = 'options'.freeze
19
+ LINKS_KEY = '_links'.freeze
20
+ EMBEDDED_KEY = '_embedded'.freeze
21
+ CURIE_KEY = 'curies'.freeze
22
+ HREF = 'href'.freeze
23
+ @reserved_keys = [LINKS_KEY, EMBEDDED_KEY, META_KEY, REF_KEY]
24
+
25
+ RESERVED_FIELD_VALUES = Field::SIMPLE_METHODS + [Field::NAME_KEY, Field::SCOPE_KEY, Field::OPTIONS_KEY, Field::VALIDATORS_KEY, Field::DESCRIPTORS_KEY, DATA_KEY]
26
+
27
+ # This need to be public to support embedded data
28
+ # TODO: make this private
29
+ def to_representor_hash
30
+ media = @target.is_a?(Hash) ? @target : JSON.parse(@target)
31
+ builder_add_from_deserialized(RepresentorBuilder.new, media).to_representor_hash
32
+ end
33
+
34
+ private
35
+
36
+ def self.reserved_keys
37
+ @reserved_keys
38
+ end
39
+
40
+ def builder_add_from_deserialized(builder, media)
41
+ media = dereference_meta_media(media)
42
+ builder = deserialize_properties(builder, media)
43
+ builder = deserialize_links(builder, media)
44
+ builder = deserialize_embedded(builder, media)
45
+ end
46
+
47
+ # Properties are normal JSON keys in the Hale document. Create properties in the resulting object
48
+ def deserialize_properties(builder, media)
49
+ media.each do |k,v|
50
+ builder = builder.add_attribute(k, v) unless (self.class.reserved_keys.include?(k))
51
+ end
52
+ builder
53
+ end
54
+
55
+ # links are under '_links' in the original document. Links always have a key (its name) but
56
+ # the value can be a hash with its properties or an array with several links.
57
+ # TODO: Figure out error handling for malformed documents
58
+ def deserialize_links(builder, media)
59
+ links = media[LINKS_KEY] || {}
60
+
61
+ links.each do |link_rel,link_values|
62
+ raise(DeserializationError, 'CURIE support not implemented for HAL') if link_rel.eql?(CURIE_KEY)
63
+ if link_values.is_a?(Array)
64
+ if link_values.any? { |link| link[HREF].nil? }
65
+ raise DeserializationError, 'All links must contain the href attribute'
66
+ end
67
+ builder = builder.add_transition_array(link_rel, link_values)
68
+ else
69
+ href = link_values[HREF]
70
+ raise DeserializationError, 'All links must contain the href attribute' unless href
71
+ builder = builder.add_transition(link_rel, href, link_values )
72
+ end
73
+ end
74
+
75
+ builder
76
+ end
77
+
78
+ # embedded resources are under '_embedded' in the original document, similarly to links they can
79
+ # contain an array or a single embedded resource. An embedded resource is a full document so
80
+ # we create a new HaleDeserializer for each.
81
+ def deserialize_embedded(builder, media)
82
+ make_embedded_resource = ->(x) { self.class.new(x).to_representor_hash.to_h }
83
+ (media[EMBEDDED_KEY] || {}).each do |name, value|
84
+ resource_hash = map_or_apply(make_embedded_resource, value)
85
+ builder = builder.add_embedded(name, resource_hash)
86
+ end
87
+ builder
88
+ end
89
+
90
+ def deserialize_links(builder, media)
91
+ (media[LINKS_KEY] || {}).each do |link_rel, link_values|
92
+ link_values = [link_values] unless link_values.is_a?(Array)
93
+ ensure_valid_links!(link_rel, link_values)
94
+ link_values = parse_validators(link_values)
95
+ link_values = parse_options(link_values)
96
+ builder = builder.add_transition_array(link_rel, link_values)
97
+ end
98
+ builder
99
+ end
100
+
101
+ def ensure_valid_links!(link_rel, link_values_array)
102
+ raise(DeserializationError, 'CURIE support not implemented for HAL') if link_rel.eql?(CURIE_KEY)
103
+
104
+ if link_values_array.map { |link| link[HREF] }.any?(&:nil?)
105
+ raise DeserializationError, 'All links must contain the href attribute'
106
+ end
107
+ end
108
+
109
+ def deep_find_and_transform!(obj, target_key, &blk)
110
+ if obj.respond_to?(:key) && obj.key?(target_key)
111
+ deep_find_and_transform!(obj[target_key], target_key, &blk)
112
+ yield obj
113
+ elsif [Array, Hash].include?(obj.class)
114
+ obj.each { |*el| deep_find_and_transform!(el.last, target_key, &blk) }
115
+ end
116
+ end
117
+
118
+ def dereference_meta_media(media)
119
+ media = deep_dup(media)
120
+ # Remove _meta from media to prevent serialization as property
121
+ if meta_info = media.delete(META_KEY)
122
+ deep_find_and_transform!(media, REF_KEY) do |media|
123
+ media.delete(REF_KEY).each { |ref| media[ref] = meta_info[ref] }
124
+ end
125
+ end
126
+ media
127
+ end
128
+
129
+ def parse_options(media)
130
+ media = deep_dup(media)
131
+ deep_find_and_transform!(media, OPTIONS_KEY) { |media| parse_options!(media) }
132
+ media
133
+ end
134
+
135
+ def parse_options!(media)
136
+ if media[OPTIONS_KEY].is_a?(Array) && media[OPTIONS_KEY].first.is_a?(Hash)
137
+ new_options = media[OPTIONS_KEY].reduce({}) { |memo, hash| memo.merge!(hash) }
138
+ media[OPTIONS_KEY] = { 'hash' => new_options }
139
+ elsif !media[OPTIONS_KEY].is_a?(Hash)
140
+ media[OPTIONS_KEY] = { 'list' => deep_dup(media[OPTIONS_KEY]) }
141
+ end
142
+ end
143
+
144
+ def parse_validators(media)
145
+ media = deep_dup(media)
146
+ deep_find_and_transform!(media, DATA_KEY) { |media| parse_data!(media) }
147
+ media
148
+ end
149
+
150
+ def parse_data!(media)
151
+ media[DATA_KEY].each do |field_key, field_value|
152
+ arr = []
153
+ field_value.each do |k,v|
154
+ arr << {k => media[DATA_KEY][field_key].delete(k)} unless RESERVED_FIELD_VALUES.include?(k.to_sym)
155
+ end
156
+ media[DATA_KEY][field_key][Field::VALIDATORS_KEY] = arr unless arr.empty?
157
+ end
158
+ end
159
+
160
+ end
161
+
162
+ end
@@ -0,0 +1,110 @@
1
+ require 'representors/serialization/hal_serializer'
2
+
3
+ module Representors
4
+ module Serialization
5
+ class HaleSerializer < HalSerializer
6
+ media_symbol :hale_json
7
+ media_type 'application/vnd.hale+json'
8
+
9
+ SEMANTIC_TYPES = {
10
+ select: "text", #No way in Crichton to distinguish [Int] and [String]
11
+ search:"text",
12
+ text: "text",
13
+ boolean: "bool", #a Server should accept ?cat&dog or ?cat=cat&dog=dog
14
+ number: "number",
15
+ email: "text",
16
+ tel: "text",
17
+ datetime: "text",
18
+ time: "text",
19
+ date: "text",
20
+ month: "text",
21
+ week: "text",
22
+ object: "object",
23
+ :"datetime-local" => "text"
24
+ }
25
+ # This is public and returning a hash to be able to implement embedded resources
26
+ # serialization
27
+ # TODO: make this private and merge with to_media_type
28
+ # The name is quite misleading,
29
+ def to_hash(options ={})
30
+ base_hash, links, embedded_hales = common_serialization(@target)
31
+ meta = get_data_lists(@target)
32
+ base_hash.merge!(meta).merge!(links).merge!(embedded_hales.(options))
33
+ base_hash
34
+ end
35
+
36
+
37
+ # This is the main entry of this class. It returns a serialization of the data
38
+ # in a given media type.
39
+ def to_media_type(options = {})
40
+ to_hash(options).to_json
41
+ end
42
+
43
+ private
44
+
45
+ def get_data_lists(representor)
46
+ meta = {}
47
+ representor.datalists.each do |datalist|
48
+ meta[datalist.id] = datalist.to_data
49
+ end
50
+ meta.empty? ? {} : {'_meta' => meta }
51
+ end
52
+
53
+ def elemental_renderer(etype)
54
+ {
55
+ type: ->(element) { render_type(element.field_type,element.type) if element.field_type || element.type },
56
+ scope: ->(element) { element.scope unless element.scope == 'attribute' },
57
+ value: ->(element) { element.value unless element.value.nil? },
58
+ multi: ->(element) { true if element.cardinality == "multiple" },
59
+ data: ->(element) { render_data_elements(element.descriptors) if element.type == 'object' },
60
+ }[etype]
61
+ end
62
+
63
+ def get_data_validators(element)
64
+ element.validators.reduce({}) do |results, validator|
65
+ results.merge( validator.is_a?(Hash) ? validator : {validator => true} )
66
+ end
67
+ end
68
+
69
+ def get_data_properties(element)
70
+ [:type, :scope, :value, :multi, :data].reduce({}) do |result, symbol|
71
+ elemental = elemental_renderer(symbol).call(element)
72
+ result.merge( elemental.nil? ? {} : {symbol => elemental} )
73
+ end
74
+ end
75
+
76
+ def get_data_element(element)
77
+ options = if element.options.datalist?
78
+ { '_ref' => [element.options.id] }
79
+ elsif element.options.type == Representors::Options::HASH_TYPE
80
+ element.options.to_hash.map { |option| Hash[*option] }
81
+ else
82
+ element.options.to_list
83
+ end
84
+ element_data = get_data_validators(element)
85
+ elementals = get_data_properties(element)
86
+ elementals[:options] = options unless options.empty?
87
+ { element.name => element_data.merge(elementals) }
88
+ end
89
+
90
+ def render_data_elements(elements)
91
+ elements.reduce({}) do |results, element|
92
+ results.merge( get_data_element(element) )
93
+ end
94
+ end
95
+
96
+ def build_links_for_this_media_type(transition)
97
+ link = super(transition) #default Hal serialization
98
+ # below add fields specific for Hale
99
+ data_elements = render_data_elements(transition.descriptors)
100
+ link[:data] = data_elements unless data_elements.empty?
101
+ link[:method] = transition.interface_method unless transition.interface_method == Transition::DEFAULT_METHOD
102
+ link
103
+ end
104
+
105
+ def render_type(field_type, type = SEMANTIC_TYPES[field_type.to_sym])
106
+ field_type ? "#{type}:#{field_type}" : "#{type}"
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,27 @@
1
+ require 'representor_support/utilities'
2
+
3
+ module Representors
4
+ class SerializationBase
5
+ include RepresentorSupport::Utilities
6
+
7
+ attr_reader :target
8
+
9
+ def self.media_symbols
10
+ @media_symbols ||= Set.new
11
+ end
12
+
13
+ def self.media_types
14
+ @media_types ||= Set.new
15
+ end
16
+
17
+ private
18
+ def self.media_symbol(*symbol)
19
+ @media_symbols = media_symbols | symbol
20
+ end
21
+
22
+ def self.media_type(*media)
23
+ @media_types = media_types | media
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ require 'representors/errors'
2
+
3
+ module Representors
4
+ ##
5
+ # Base class for factories that manages the registration of serialization classes and the common factory interface.
6
+ class SerializationFactoryBase
7
+ def self.build(media_type, object)
8
+ klass = serialization_class(media_type)
9
+ if klass
10
+ klass.new(object)
11
+ else
12
+ raise UnknownMediaTypeError, "Unknown media-type: #{media_type}."
13
+ end
14
+ end
15
+
16
+ def self.media_symbol_mapping
17
+ @media_symbol_mapping ||= registered_serialization_classes.map do |serialization_class|
18
+ serialization_class.media_symbols.map { |media_symbol| { media_symbol => serialization_class } }.reduce(:merge)
19
+ end.reduce(:merge)
20
+ end
21
+
22
+ def self.media_type_mapping
23
+ @media_type_mapping ||= registered_serialization_classes.map do |serializer|
24
+ serializer.media_types.map { |media_type| { media_type => serializer.media_symbols.first } }.reduce(:merge)
25
+ end.reduce(:merge)
26
+ end
27
+
28
+
29
+ private
30
+ def self.register_serialization_classes(*serializers)
31
+ clear_memoization
32
+ @_registered_serialization_classes ||= []
33
+ @_registered_serialization_classes |= serializers
34
+ end
35
+
36
+ def self.clear_memoization
37
+ @registered_serialization_classes = nil
38
+ @media_symbol_mapping = nil
39
+ @media_type_mapping = nil
40
+ end
41
+
42
+ def self.registered_serialization_classes
43
+ @registered_serialization_classes ||= @_registered_serialization_classes.dup.freeze
44
+ end
45
+
46
+ # If a client send directly a Content-Type it may have encodings or other things so we want
47
+ # to be more flexible
48
+ def self.serialization_class(media_type)
49
+ symbol = media_type.is_a?(Symbol) ? media_type : media_type_mapping[media_type]
50
+ media_symbol_mapping[symbol]
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,20 @@
1
+ require 'representors/serialization/serialization_base'
2
+ require 'representors/serialization/serializer_factory'
3
+
4
+ module Representors
5
+ class SerializerBase < SerializationBase
6
+
7
+ def initialize(target)
8
+ @target = target.empty? ? Representor.new({}) : target
9
+ end
10
+
11
+ def self.inherited(subclass)
12
+ SerializerFactory.register_serializers(subclass)
13
+ end
14
+
15
+ def to_hash(options = {})
16
+ raise "Abstract method #to_hash not implemented in #{self.class.to_s} serializer class."
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ require 'representors/serialization/serialization_factory_base'
2
+
3
+ module Representors
4
+ class SerializerFactory < SerializationFactoryBase
5
+ def self.register_serializers(*serializers)
6
+ register_serialization_classes(*serializers)
7
+ end
8
+
9
+ def self.registered_serializers
10
+ registered_serialization_classes
11
+ end
12
+
13
+ def self.serializer?(serializer_name)
14
+ registered_serializers.any? { |serializer| serializer.media_symbol.include?(serializer_name.to_sym) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,130 @@
1
+ require 'addressable/template'
2
+
3
+ module Representors
4
+ ##
5
+ # Manages the respresentation of link elements for hypermedia messages.
6
+ class Transition
7
+ REL_KEY = :rel
8
+ HREF_KEY = :href
9
+ LINKS_KEY = :links
10
+ METHOD_KEY = :method
11
+ DESCRIPTORS_KEY = :descriptors
12
+ DEFAULT_METHOD = 'GET'
13
+ PARAMETER_FIELDS = 'href'
14
+ ATTRIBUTE_FIELDS = 'attribute'
15
+ URL_TEMPLATE = "%s{?%s}"
16
+
17
+ # @example
18
+ # hash = {rel: "self", href: "http://example.org"}
19
+ # Transition.new(hash)
20
+ # Must contain at least the property :href
21
+ # @param [Hash] the abstract representor hash defining a transition
22
+ def initialize(transition_hash)
23
+ @transition_hash = transition_hash
24
+ end
25
+
26
+ # @return [String] so the user can 'puts' this object
27
+ def to_s
28
+ @transition_hash.inspect
29
+ end
30
+
31
+ # @return [Hash] useful in cucumber steps where the feature file provides a hash
32
+ def to_hash
33
+ Hash[@transition_hash.map{ |k, v| [k.to_s, v] }]
34
+ end
35
+
36
+ # @return [String] The name of the Relationship
37
+ def rel
38
+ retrieve(REL_KEY)
39
+ end
40
+
41
+ # @return [String] The URI for the object
42
+ def uri(data={})
43
+ template = Addressable::Template.new(retrieve(HREF_KEY))
44
+ template.expand(data).to_str
45
+ end
46
+
47
+ # @param [String] key on the transitions hash to retrieve
48
+ # @return [String] with the value of the key
49
+ def [](key)
50
+ retrieve(key)
51
+ end
52
+
53
+ # @param [String] key on the transitions hash to retrieve
54
+ # @return [Bool] false if there is no key
55
+ def has_key?(key)
56
+ !retrieve(key).nil?
57
+ end
58
+
59
+ # @return [String] The URI for the object templated against #parameters
60
+ def templated_uri
61
+ #URL as it is, it will be the templated URL of the document if it was templated
62
+ retrieve(HREF_KEY)
63
+ end
64
+
65
+ def templated?
66
+ # if we have any variable then it is not a templated url
67
+ !Addressable::Template.new(retrieve(HREF_KEY)).variables.empty?
68
+ end
69
+
70
+ # @return [Array] who's elements are all <Crichton:Transition> objects
71
+ def meta_links
72
+ @meta_links ||= (retrieve(LINKS_KEY) || []).map do |link_key, link_href|
73
+ Transition.new({rel: link_key, href: link_href})
74
+ end
75
+ end
76
+
77
+ # @return [String] representing the Uniform Interface Method
78
+ def interface_method
79
+ retrieve(METHOD_KEY) || DEFAULT_METHOD
80
+ end
81
+ # The Parameters (i.e. GET variables)
82
+ #
83
+ # @return [Array] who's elements are all <Crichton:Field> objects
84
+ # Variables in the URI template rules this method, we are going to return a field for each of them
85
+ # if we find a field inside the 'data' of the document describing that variable, we use that information
86
+ # else we return a field with default information about a variable.
87
+ def parameters
88
+ data_fields = descriptors_fields.select{|field| field.scope == PARAMETER_FIELDS }
89
+ Addressable::Template.new(retrieve(HREF_KEY)).variables.map do |template_variable_name|
90
+ field_specified = data_fields.find{|field| field.name.to_s == template_variable_name.to_s}
91
+ if field_specified
92
+ field_specified
93
+ else
94
+ Field.new({template_variable_name.to_sym => {type: 'string', scope: 'href'}})
95
+ end
96
+ end
97
+ # descriptors_fields.select{|field| field.scope == PARAMETER_FIELDS }
98
+ end
99
+
100
+ # The Parameters (i.e. POST variables)
101
+ #
102
+ # @return [Array] who's elements are all <Crichton:Field> objects
103
+ def attributes
104
+ @attributes ||= descriptors_fields.select{|field| field.scope == ATTRIBUTE_FIELDS }
105
+ end
106
+
107
+ # The Parameters (i.e. GET variables)
108
+ #
109
+ # @return [Array] who's elements are all <Crichton:Field> objects
110
+ def descriptors
111
+ @descriptions ||= (attributes + parameters)
112
+ end
113
+
114
+ private
115
+
116
+ def descriptors_fields
117
+ @fields_hash ||= descriptors_hash.map { |k, v| Field.new({k => v }) }
118
+ end
119
+
120
+ def descriptors_hash
121
+ @transition_hash[DESCRIPTORS_KEY] || []
122
+ end
123
+
124
+ # accept retrieving keys by symbol or string
125
+ def retrieve(key)
126
+ @transition_hash[key.to_sym] || @transition_hash[key.to_s]
127
+ end
128
+
129
+ end
130
+ end