vorpal 1.0.2 → 1.3.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -26
  3. data/lib/vorpal/aggregate_mapper.rb +13 -2
  4. data/lib/vorpal/aggregate_traversal.rb +9 -8
  5. data/lib/vorpal/config/association_config.rb +84 -0
  6. data/lib/vorpal/config/belongs_to_config.rb +35 -0
  7. data/lib/vorpal/config/class_config.rb +71 -0
  8. data/lib/vorpal/config/configs.rb +54 -0
  9. data/lib/vorpal/config/foreign_key_info.rb +23 -0
  10. data/lib/vorpal/config/has_many_config.rb +38 -0
  11. data/lib/vorpal/config/has_one_config.rb +35 -0
  12. data/lib/vorpal/config/main_config.rb +68 -0
  13. data/lib/vorpal/db_loader.rb +25 -22
  14. data/lib/vorpal/driver/postgresql.rb +42 -6
  15. data/lib/vorpal/dsl/config_builder.rb +26 -73
  16. data/lib/vorpal/dsl/configuration.rb +139 -42
  17. data/lib/vorpal/dsl/defaults_generator.rb +1 -1
  18. data/lib/vorpal/engine.rb +27 -13
  19. data/lib/vorpal/exceptions.rb +4 -0
  20. data/lib/vorpal/identity_map.rb +7 -2
  21. data/lib/vorpal/loaded_objects.rb +57 -14
  22. data/lib/vorpal/util/array_hash.rb +22 -8
  23. data/lib/vorpal/util/hash_initialization.rb +1 -1
  24. data/lib/vorpal/version.rb +1 -1
  25. data/vorpal.gemspec +4 -5
  26. metadata +18 -78
  27. data/.editorconfig +0 -13
  28. data/.envrc +0 -4
  29. data/.gitignore +0 -16
  30. data/.rspec +0 -1
  31. data/.ruby-version +0 -1
  32. data/.travis.yml +0 -18
  33. data/.yardopts +0 -1
  34. data/Appraisals +0 -18
  35. data/Gemfile +0 -4
  36. data/Rakefile +0 -39
  37. data/bin/appraisal +0 -29
  38. data/bin/rake +0 -29
  39. data/bin/rspec +0 -29
  40. data/docker-compose.yml +0 -19
  41. data/gemfiles/rails_5_1.gemfile +0 -11
  42. data/gemfiles/rails_5_1.gemfile.lock +0 -101
  43. data/gemfiles/rails_5_2.gemfile +0 -11
  44. data/gemfiles/rails_5_2.gemfile.lock +0 -101
  45. data/gemfiles/rails_6_0.gemfile +0 -9
  46. data/gemfiles/rails_6_0.gemfile.lock +0 -101
  47. data/lib/vorpal/configs.rb +0 -296
  48. data/spec/acceptance/vorpal/aggregate_mapper_spec.rb +0 -910
  49. data/spec/helpers/codecov_helper.rb +0 -7
  50. data/spec/helpers/db_helpers.rb +0 -69
  51. data/spec/helpers/profile_helpers.rb +0 -26
  52. data/spec/integration/vorpal/driver/postgresql_spec.rb +0 -42
  53. data/spec/integration_spec_helper.rb +0 -29
  54. data/spec/performance/vorpal/performance_spec.rb +0 -305
  55. data/spec/unit/vorpal/configs_spec.rb +0 -117
  56. data/spec/unit/vorpal/db_loader_spec.rb +0 -103
  57. data/spec/unit/vorpal/dsl/config_builder_spec.rb +0 -18
  58. data/spec/unit/vorpal/dsl/defaults_generator_spec.rb +0 -75
  59. data/spec/unit/vorpal/identity_map_spec.rb +0 -62
  60. data/spec/unit/vorpal/loaded_objects_spec.rb +0 -22
  61. data/spec/unit/vorpal/util/string_utils_spec.rb +0 -25
  62. data/spec/unit_spec_helper.rb +0 -1
@@ -1,57 +1,154 @@
1
1
  require 'vorpal/engine'
2
2
  require 'vorpal/dsl/config_builder'
3
3
  require 'vorpal/driver/postgresql'
4
+ require 'vorpal/config/main_config'
4
5
 
5
6
  module Vorpal
6
7
  module Dsl
7
- module Configuration
8
-
9
- # Configures and creates a {Engine} instance.
10
- #
11
- # @param options [Hash] Global configuration options for the engine instance.
12
- # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
13
- # Must be duck-type compatible with {Postgresql}.
8
+ # Implements the Vorpal DSL.
14
9
  #
15
- # @return [Engine] Instance of the mapping engine.
16
- def define(options={}, &block)
17
- master_config = build_config(&block)
18
- db_driver = options.fetch(:db_driver, Driver::Postgresql.new)
19
- Engine.new(db_driver, master_config)
20
- end
21
-
22
- # Maps a domain class to a relational table.
10
+ # ```ruby
11
+ # engine = Vorpal.define do
12
+ # map Tree do
13
+ # attributes :name
14
+ # belongs_to :trunk
15
+ # has_many :branches
16
+ # end
23
17
  #
24
- # @param domain_class [Class] Type of the domain model to be mapped
25
- # @param options [Hash] Configure how to map the domain model
26
- # @option options [String] :to
27
- # Class of the ActiveRecord object that will map this domain class to the DB.
28
- # Optional, if one is not specified, it will be generated.
29
- # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly)
30
- # Object that will convert the domain objects into a hash.
18
+ # map Trunk do
19
+ # attributes :length
20
+ # has_one :tree
21
+ # end
31
22
  #
32
- # Must have a `(Hash) serialize(Object)` method.
33
- # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly)
34
- # Object that will set a hash of attribute_names->values onto a new domain
35
- # object.
23
+ # map Branch do
24
+ # attributes :length
25
+ # belongs_to :tree
26
+ # end
27
+ # end
36
28
  #
37
- # Must have a `(Object) deserialize(Object, Hash)` method.
38
- def map(domain_class, options={}, &block)
39
- @class_configs << build_class_config(domain_class, options, &block)
40
- end
29
+ # mapper = engine.mapper_for(Tree)
30
+ # ```
31
+ module Configuration
32
+ # Configures and creates a {Engine} instance.
33
+ #
34
+ # @param options [Hash] Global configuration options for the engine instance.
35
+ # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
36
+ # Must be duck-type compatible with {Postgresql}.
37
+ #
38
+ # @return [Engine] Instance of the mapping engine.
39
+ def define(options={}, &block)
40
+ @main_config = Config::MainConfig.new
41
+ instance_exec(&block)
42
+ @main_config.initialize_association_configs
43
+ db_driver = options.fetch(:db_driver, Driver::Postgresql.new)
44
+ engine = Engine.new(db_driver, @main_config)
45
+ @main_config = nil # make sure this MainConfig is never re-used by accident.
46
+ engine
47
+ end
41
48
 
42
- # @private
43
- def build_class_config(domain_class, options={}, &block)
44
- builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new)
45
- builder.instance_exec(&block) if block_given?
46
- builder.build
47
- end
49
+ # Maps a domain class to a relational table.
50
+ #
51
+ # @param domain_class [Class] Type of the domain model to be mapped
52
+ # @param options [Hash] Configure how to map the domain model
53
+ # @option options [String] :to
54
+ # Class of the ActiveRecord object that will map this domain class to the DB.
55
+ # Optional, if one is not specified, it will be generated.
56
+ # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly)
57
+ # Object that will convert the domain objects into a hash.
58
+ #
59
+ # Must have a `(Hash) serialize(Object)` method.
60
+ # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly)
61
+ # Object that will set a hash of attribute_names->values onto a new domain
62
+ # object.
63
+ #
64
+ # Must have a `(Object) deserialize(Object, Hash)` method.
65
+ # @option options [Symbol] :primary_key_type [:serial, :uuid] (:serial)
66
+ # The type of primary key for the class. :serial for auto-incrementing integer, :uuid for a UUID
67
+ # @option options [Symbol] :id
68
+ # Same as :primary_key_type. Exists for compatibility with the Rails API.
69
+ def map(domain_class, options={}, &block)
70
+ class_config = build_class_config(domain_class, options, &block)
71
+ @main_config.add_class_config(class_config)
72
+ class_config
73
+ end
48
74
 
49
- # @private
50
- def build_config(&block)
51
- @class_configs = []
52
- self.instance_exec(&block)
53
- MasterConfig.new(@class_configs)
75
+ # @private
76
+ def build_class_config(domain_class, options, &block)
77
+ @builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new)
78
+ instance_exec(&block) if block_given?
79
+ class_config = @builder.build
80
+ @builder = nil # make sure this ConfigBuilder is never re-used by accident.
81
+ class_config
82
+ end
83
+
84
+ # Maps the given attributes to and from the domain object and the DB. Not needed
85
+ # if a serializer and deserializer were provided.
86
+ def attributes(*attributes)
87
+ @builder.attributes(*attributes)
88
+ end
89
+
90
+ # Defines a one-to-many association to another type where the foreign key is stored on the associated table.
91
+ #
92
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
93
+ # traversed in one direction: from the type that defines the association (the one with the
94
+ # getter) to the type that is associated.
95
+ #
96
+ # @param name [String] Name of the association getter.
97
+ # @param options [Hash]
98
+ # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
99
+ # @option options [String] :fk (Association-owning class name converted to snakecase and appended with a '_id') The name of the DB column on the associated table that contains the foreign key reference to the association owner.
100
+ # @option options [String] :fk_type The name of the DB column on the associated table that contains the association-owning class name. Only needed when the associated end is polymorphic.
101
+ # @option options [String] :unique_key_name ("id") The name of the column on the owning table that the foreign key points to. Normally the primary key column.
102
+ # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API.
103
+ # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class.
104
+ # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class.
105
+ def has_many(name, options={})
106
+ @builder.has_many(name, options)
107
+ end
108
+
109
+ # Defines a one-to-one association to another type where the foreign key
110
+ # is stored on the associated table.
111
+ #
112
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
113
+ # traversed in one direction: from the type that defines the association (the one with the
114
+ # getter) to the type that is associated.
115
+ #
116
+ # @param name [String] Name of the association getter.
117
+ # @param options [Hash]
118
+ # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
119
+ # @option options [String] :fk (Association-owning class name converted to snakecase and appended with a '_id') The name of the DB column on the associated table that contains the foreign key reference to the association owner.
120
+ # @option options [String] :fk_type The name of the DB column on the associated table that contains the association-owning class name. Only needed when the associated end is polymorphic.
121
+ # @option options [String] :unique_key_name ("id") The name of the column on the owning table that the foreign key points to. Normally the primary key column.
122
+ # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API.
123
+ # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class.
124
+ # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class.
125
+ def has_one(name, options={})
126
+ @builder.has_one(name, options)
127
+ end
128
+
129
+ # Defines a one-to-one association with another type where the foreign key
130
+ # is stored on the table of the entity declaring the association.
131
+ #
132
+ # This association can be polymorphic. I.E. associates can be of different types.
133
+ #
134
+ # In Object-Oriented programming, associations are *directed*. This means that they can only be
135
+ # traversed in one direction: from the type that defines the association (the one with the
136
+ # getter) to the type that is associated.
137
+ #
138
+ # @param name [String] Name of the association getter.
139
+ # @param options [Hash]
140
+ # @option options [Boolean] :owned (True) True if the associated type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted.
141
+ # @option options [String] :fk (Associated class name converted to snakecase and appended with a '_id') The name of the DB column on the association-owning table that contains the foreign key reference to the associated table.
142
+ # @option options [String] :fk_type The name of the DB column on the association-owning table that contains the associated class name. Only needed when the association is polymorphic.
143
+ # @option options [String] :unique_key_name ("id") The name of the column on the associated table that the foreign key points to. Normally the primary key column.
144
+ # @option options [String] :primary_key Same as :unique_key_name. Exists for compatibility with Rails API.
145
+ # @option options [Class] :child_class DEPRECATED. Use `associated_class` instead. The associated class.
146
+ # @option options [Class] :associated_class (Name of the association converted to a Class) The associated class.
147
+ # @option options [[Class]] :child_classes DEPRECATED. Use `associated_classes` instead. The list of possible classes that can be associated. This is for polymorphic associations. Takes precedence over `:associated_class`.
148
+ # @option options [[Class]] :associated_classes (Name of the association converted to a Class) The list of possible classes that can be associated. This is for polymorphic associations. Takes precedence over `:associated_class`.
149
+ def belongs_to(name, options={})
150
+ @builder.belongs_to(name, options)
151
+ end
54
152
  end
55
153
  end
56
- end
57
154
  end
@@ -35,7 +35,7 @@ module Vorpal
35
35
  ActiveSupport::Inflector.foreign_key(name.to_s)
36
36
  end
37
37
 
38
- def child_class(association_name)
38
+ def associated_class(association_name)
39
39
  module_parent.const_get(ActiveSupport::Inflector.classify(association_name.to_s))
40
40
  end
41
41
 
data/lib/vorpal/engine.rb CHANGED
@@ -7,9 +7,9 @@ require 'vorpal/exceptions'
7
7
  module Vorpal
8
8
  class Engine
9
9
  # @private
10
- def initialize(db_driver, master_config)
10
+ def initialize(db_driver, main_config)
11
11
  @db_driver = db_driver
12
- @configs = master_config
12
+ @configs = main_config
13
13
  end
14
14
 
15
15
  # Creates a mapper for saving/updating/loading/destroying an aggregate to/from
@@ -34,6 +34,9 @@ module Vorpal
34
34
  serialize(all_owned_objects, mapping, loaded_db_objects)
35
35
  new_objects = get_unsaved_objects(mapping.keys)
36
36
  begin
37
+ # Primary keys are set eagerly (instead of waiting for them to be set by ActiveRecord upon create)
38
+ # because we want to support non-null FK constraints without needing to figure the correct
39
+ # order to save entities in.
37
40
  set_primary_keys(all_owned_objects, mapping)
38
41
  set_foreign_keys(all_owned_objects, mapping)
39
42
  remove_orphans(mapping, loaded_db_objects)
@@ -89,10 +92,16 @@ module Vorpal
89
92
  @configs.config_for(domain_class).db_class
90
93
  end
91
94
 
95
+ # Try to use {AggregateMapper#query} instead.
92
96
  def query(domain_class)
93
97
  @db_driver.query(@configs.config_for(domain_class).db_class, mapper_for(domain_class))
94
98
  end
95
99
 
100
+ # @private
101
+ def class_config(domain_class)
102
+ @configs.config_for(domain_class)
103
+ end
104
+
96
105
  private
97
106
 
98
107
  def wrap(collection_or_not)
@@ -128,8 +137,9 @@ module Vorpal
128
137
  loaded_db_objects.each do |config, db_objects|
129
138
  db_objects.each do |db_object|
130
139
  config.local_association_configs.each do |association_config|
131
- db_remote = loaded_db_objects.find_by_id(
140
+ db_remote = loaded_db_objects.find_by_unique_key(
132
141
  association_config.remote_class_config(db_object),
142
+ association_config.unique_key_name,
133
143
  association_config.fk_value(db_object)
134
144
  )
135
145
  association_config.associate(identity_map.get(db_object), identity_map.get(db_remote))
@@ -150,10 +160,10 @@ module Vorpal
150
160
  def serialize_object(object, config, loaded_db_objects)
151
161
  if config.serialization_required?
152
162
  attributes = config.serialize(object)
153
- if object.id.nil?
163
+ db_object = loaded_db_objects.find_by_primary_key(config, object)
164
+ if object.id.nil? || db_object.nil? # object doesn't exist in the DB
154
165
  config.build_db_object(attributes)
155
166
  else
156
- db_object = loaded_db_objects.find_by_id(config, object.id)
157
167
  config.set_db_object_attributes(db_object, attributes)
158
168
  db_object
159
169
  end
@@ -165,7 +175,11 @@ module Vorpal
165
175
  def set_primary_keys(owned_objects, mapping)
166
176
  owned_objects.each do |config, objects|
167
177
  in_need_of_primary_keys = objects.find_all { |obj| obj.id.nil? }
168
- primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length)
178
+ if config.primary_key_type == :uuid
179
+ primary_keys = Array.new(in_need_of_primary_keys.length) { SecureRandom.uuid }
180
+ elsif config.primary_key_type == :serial
181
+ primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length)
182
+ end
169
183
  in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key|
170
184
  mapping[object].id = primary_key
171
185
  object.id = primary_key
@@ -179,23 +193,23 @@ module Vorpal
179
193
  objects.each do |object|
180
194
  config.has_manys.each do |has_many_config|
181
195
  if has_many_config.owned
182
- children = has_many_config.get_children(object)
183
- children.each do |child|
184
- has_many_config.set_foreign_key(mapping[child], object)
196
+ associates = has_many_config.get_associated(object)
197
+ associates.each do |associate|
198
+ has_many_config.set_foreign_key(mapping[associate], object)
185
199
  end
186
200
  end
187
201
  end
188
202
 
189
203
  config.has_ones.each do |has_one_config|
190
204
  if has_one_config.owned
191
- child = has_one_config.get_child(object)
192
- has_one_config.set_foreign_key(mapping[child], object)
205
+ associate = has_one_config.get_associated(object)
206
+ has_one_config.set_foreign_key(mapping[associate], object) if associate
193
207
  end
194
208
  end
195
209
 
196
210
  config.belongs_tos.each do |belongs_to_config|
197
- child = belongs_to_config.get_child(object)
198
- belongs_to_config.set_foreign_key(mapping[object], child)
211
+ associate = belongs_to_config.get_associated(object)
212
+ belongs_to_config.set_foreign_key(mapping[object], associate)
199
213
  end
200
214
  end
201
215
  end
@@ -4,4 +4,8 @@ module Vorpal
4
4
  class InvalidAggregateRoot < StandardError; end
5
5
 
6
6
  class ConfigurationNotFound < StandardError; end
7
+
8
+ class ConfigurationError < StandardError; end
9
+
10
+ class InvariantViolated < StandardError; end
7
11
  end
@@ -28,9 +28,14 @@ module Vorpal
28
28
 
29
29
  def key(db_row)
30
30
  return nil unless db_row
31
- raise "Cannot map a DB row without an id '#{db_row.inspect}' to an entity." if db_row.id.nil?
31
+ primary_key_value = get_primary_key_value(db_row)
32
+ raise "Cannot map a DB row without an id '#{db_row.inspect}' to an entity." if primary_key_value.nil?
32
33
  raise "Cannot map a DB row without a Class with a name '#{db_row.inspect}' to an entity." if db_row.class.name.nil?
33
- [db_row.id, db_row.class.name]
34
+ [primary_key_value, db_row.class.name]
35
+ end
36
+
37
+ def get_primary_key_value(db_row)
38
+ db_row.send(db_row.class.primary_key)
34
39
  end
35
40
  end
36
41
  end
@@ -5,37 +5,80 @@ module Vorpal
5
5
 
6
6
  # @private
7
7
  class LoadedObjects
8
- include Util::ArrayHash
9
8
  extend Forwardable
10
9
  include Enumerable
11
10
 
12
- attr_reader :objects
13
- def_delegators :objects, :each
11
+ def_delegators :@objects, :each
14
12
 
15
13
  def initialize
16
- @objects = Hash.new([])
17
- @objects_by_id = Hash.new
14
+ @objects = Util::ArrayHash.new
15
+ @cache = {}
18
16
  end
19
17
 
20
18
  def add(config, objects)
21
19
  objects_to_add = objects.map do |object|
22
- if !already_loaded?(config, object.id)
23
- @objects_by_id[[config.domain_class.name, object.id]] = object
20
+ if !already_loaded?(config, object)
21
+ add_to_cache(config, object)
24
22
  end
25
- end
26
- add_to_hash(@objects, config, objects_to_add.compact)
23
+ end.compact
24
+ @objects.append(config, objects_to_add)
25
+ objects_to_add
26
+ end
27
+
28
+ def find_by_primary_key(config, object)
29
+ find_by_unique_key(config, "id", object.id)
27
30
  end
28
31
 
29
- def find_by_id(config, id)
30
- @objects_by_id[[config.domain_class.name, id]]
32
+ def find_by_unique_key(config, column_name, value)
33
+ get_from_cache(config, column_name, value)
31
34
  end
32
35
 
33
36
  def all_objects
34
- @objects_by_id.values
37
+ @objects.values
38
+ end
39
+
40
+ def already_loaded_by_unique_key?(config, column_name, id)
41
+ !find_by_unique_key(config, column_name, id).nil?
42
+ end
43
+
44
+ private
45
+
46
+ def already_loaded?(config, object)
47
+ !find_by_primary_key(config, object).nil?
35
48
  end
36
49
 
37
- def already_loaded?(config, id)
38
- !find_by_id(config, id).nil?
50
+ # TODO: Do we have to worry about symbols vs strings for the column_name?
51
+ def add_to_cache(config, object)
52
+ # we take a shortcut here assuming that the cache has already been primed with the primary key column
53
+ # because this method should always be guarded by #already_loaded?
54
+ column_cache = @cache[config]
55
+ column_cache.each do |column_name, unique_key_cache|
56
+ unique_key_cache[object.send(column_name)] = object
57
+ end
58
+ object
59
+ end
60
+
61
+ def get_from_cache(config, column_name, value)
62
+ lookup_hash(config, column_name)[value]
63
+ end
64
+
65
+ # lazily primes the cache
66
+ # TODO: Do we have to worry about symbols vs strings for the column_name?
67
+ def lookup_hash(config, column_name)
68
+ column_cache = @cache[config]
69
+ if column_cache.nil?
70
+ column_cache = {}
71
+ @cache[config] = column_cache
72
+ end
73
+ unique_key_cache = column_cache[column_name]
74
+ if unique_key_cache.nil?
75
+ unique_key_cache = {}
76
+ column_cache[column_name] = unique_key_cache
77
+ @objects[config].each do |object|
78
+ unique_key_cache[object.send(column_name)] = object
79
+ end
80
+ end
81
+ unique_key_cache
39
82
  end
40
83
  end
41
84
  end
@@ -1,19 +1,33 @@
1
+ require 'forwardable'
2
+
1
3
  module Vorpal
2
4
  module Util
3
5
  # @private
4
- module ArrayHash
5
- def add_to_hash(array_hash, key, values)
6
- if array_hash[key].nil? || array_hash[key].empty?
7
- array_hash[key] = []
6
+ class ArrayHash
7
+ extend Forwardable
8
+
9
+ def_delegators :@hash, :each, :empty?, :[]
10
+
11
+ def initialize
12
+ @hash = Hash.new([])
13
+ end
14
+
15
+ def append(key, values)
16
+ if @hash[key].nil? || @hash[key].empty?
17
+ @hash[key] = []
8
18
  end
9
- array_hash[key].concat(Array(values))
19
+ @hash[key].concat(Array(values))
10
20
  end
11
21
 
12
- def pop(array_hash)
13
- key = array_hash.first.first
14
- values = array_hash.delete(key)
22
+ def pop
23
+ key = @hash.first.first
24
+ values = @hash.delete(key)
15
25
  [key, values]
16
26
  end
27
+
28
+ def values
29
+ @hash.values.flatten
30
+ end
17
31
  end
18
32
  end
19
33
  end