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