rom-repository 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -5
  4. data/CHANGELOG.md +24 -0
  5. data/Gemfile +21 -5
  6. data/README.md +6 -110
  7. data/lib/rom/repository/changeset/create.rb +26 -0
  8. data/lib/rom/repository/changeset/pipe.rb +40 -0
  9. data/lib/rom/repository/changeset/update.rb +82 -0
  10. data/lib/rom/repository/changeset.rb +99 -0
  11. data/lib/rom/repository/class_interface.rb +142 -0
  12. data/lib/rom/repository/command_compiler.rb +214 -0
  13. data/lib/rom/repository/command_proxy.rb +22 -0
  14. data/lib/rom/repository/header_builder.rb +13 -16
  15. data/lib/rom/repository/mapper_builder.rb +7 -14
  16. data/lib/rom/repository/{loading_proxy → relation_proxy}/wrap.rb +7 -7
  17. data/lib/rom/repository/relation_proxy.rb +225 -0
  18. data/lib/rom/repository/root.rb +110 -0
  19. data/lib/rom/repository/struct_attributes.rb +46 -0
  20. data/lib/rom/repository/struct_builder.rb +31 -14
  21. data/lib/rom/repository/version.rb +1 -1
  22. data/lib/rom/repository.rb +192 -31
  23. data/lib/rom/struct.rb +13 -8
  24. data/rom-repository.gemspec +9 -10
  25. data/spec/integration/changeset_spec.rb +86 -0
  26. data/spec/integration/command_macros_spec.rb +175 -0
  27. data/spec/integration/command_spec.rb +224 -0
  28. data/spec/integration/multi_adapter_spec.rb +3 -3
  29. data/spec/integration/repository_spec.rb +97 -2
  30. data/spec/integration/root_repository_spec.rb +88 -0
  31. data/spec/shared/database.rb +47 -3
  32. data/spec/shared/mappers.rb +35 -0
  33. data/spec/shared/models.rb +41 -0
  34. data/spec/shared/plugins.rb +66 -0
  35. data/spec/shared/relations.rb +76 -0
  36. data/spec/shared/repo.rb +38 -17
  37. data/spec/shared/seeds.rb +19 -0
  38. data/spec/spec_helper.rb +4 -1
  39. data/spec/support/mapper_registry.rb +1 -3
  40. data/spec/unit/changeset_spec.rb +58 -0
  41. data/spec/unit/header_builder_spec.rb +34 -35
  42. data/spec/unit/relation_proxy_spec.rb +170 -0
  43. data/spec/unit/sql/relation_spec.rb +5 -5
  44. data/spec/unit/struct_builder_spec.rb +7 -4
  45. data/spec/unit/struct_spec.rb +22 -0
  46. metadata +38 -41
  47. data/lib/rom/plugins/relation/key_inference.rb +0 -31
  48. data/lib/rom/repository/loading_proxy/combine.rb +0 -158
  49. data/lib/rom/repository/loading_proxy.rb +0 -182
  50. data/spec/unit/loading_proxy_spec.rb +0 -147
@@ -0,0 +1,110 @@
1
+ module ROM
2
+ class Repository
3
+ # A specialized repository type dedicated to work with a root relation
4
+ #
5
+ # This repository type builds commands and aggregates for its root relation
6
+ #
7
+ # @example
8
+ # class UserRepo < ROM::Repository[:users]
9
+ # commands :create, update: :by_pk, delete: :by_pk
10
+ # end
11
+ #
12
+ # rom = ROM.container(:sql, 'sqlite::memory') do |conf|
13
+ # conf.default.create_table(:users) do
14
+ # primary_key :id
15
+ # column :name, String
16
+ # end
17
+ # end
18
+ #
19
+ # user_repo = UserRepo.new(rom)
20
+ #
21
+ # user = user_repo.create(name: "Jane")
22
+ #
23
+ # changeset = user_repo.changeset(user.id, name: "Jane Doe")
24
+ # user_repo.update(user.id, changeset)
25
+ #
26
+ # user_repo.delete(user.id)
27
+ #
28
+ # @api public
29
+ class Root < Repository
30
+ extend ClassMacros
31
+
32
+ defines :root
33
+
34
+ # @!attribute [r] root
35
+ # @return [RelationProxy] The root relation
36
+ attr_reader :root
37
+
38
+ # Sets descendant root relation
39
+ #
40
+ # @api private
41
+ def self.inherited(klass)
42
+ super
43
+ klass.root(root)
44
+ end
45
+
46
+ # @see Repository#initialize
47
+ def initialize(container)
48
+ super
49
+ @root = relations[self.class.root]
50
+ end
51
+
52
+ # Compose a relation aggregate from the root relation
53
+ #
54
+ # @overload aggregate(*associations)
55
+ # Composes an aggregate from configured associations on the root relation
56
+ #
57
+ # @example
58
+ # user_repo.aggregate(:tasks, :posts)
59
+ #
60
+ # @param *associations [Array<Symbol>] A list of association names
61
+ #
62
+ # @overload aggregate(options)
63
+ # Composes an aggregate by delegating to combine_children method.
64
+ #
65
+ # @param options [Hash] An option hash
66
+ #
67
+ # @see RelationProxy::Combine#combine_children
68
+ #
69
+ # @return [RelationProxy]
70
+ #
71
+ # @api public
72
+ def aggregate(*args)
73
+ if args[0].is_a?(Hash) && args.size == 1
74
+ root.combine_children(args[0])
75
+ else
76
+ root.combine(*args)
77
+ end
78
+ end
79
+
80
+ # @overload changeset(name, *args)
81
+ # Delegate to Repository#changeset
82
+ # @see Repository#changeset
83
+ #
84
+ # @overload changeset(data)
85
+ # Builds a create changeset for the root relation
86
+ # @example
87
+ # user_repo.changeset(name: "Jane")
88
+ # @param data [Hash] New data
89
+ # @return [Changeset::Create]
90
+ #
91
+ # @overload changeset(restriction_arg, data)
92
+ # Builds an update changeset for the root relation
93
+ # @example
94
+ # user_repo.changeset(1, name: "Jane Doe")
95
+ # @param restriction_arg [Object] An argument for the restriction view
96
+ # @return [Changeset::Update]
97
+ #
98
+ # @override Repository#changeset
99
+ #
100
+ # @api public
101
+ def changeset(*args)
102
+ if args.first.is_a?(Symbol) && relations.key?(args.first)
103
+ super
104
+ else
105
+ super(root.name, *args)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,46 @@
1
+ module ROM
2
+ class Repository
3
+ # @api private
4
+ class StructAttributes < Module
5
+ def initialize(attributes)
6
+ super()
7
+
8
+ define_constructor(attributes)
9
+
10
+ module_eval do
11
+ include Dry::Equalizer.new(*attributes)
12
+
13
+ attr_reader(*attributes)
14
+
15
+ define_method(:to_h) do
16
+ attributes.each_with_object({}) do |attribute, h|
17
+ h[attribute] = __send__(attribute)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def define_constructor(attributes)
24
+ module_eval do
25
+ def __missing_keyword__(keyword)
26
+ raise ArgumentError.new("missing keyword: #{keyword}")
27
+ end
28
+ private :__missing_keyword__
29
+ end
30
+
31
+ kwargs = attributes.map { |a| "#{a}: __missing_keyword__(:#{a})" }.join(', ')
32
+
33
+ ivs = attributes.map { |a| "@#{a}" }.join(', ')
34
+ values = attributes.join(', ')
35
+
36
+ assignment = attributes.size > 0 ? "#{ivs} = #{values}" : EMPTY_STRING
37
+
38
+ module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
39
+ def initialize(#{kwargs})
40
+ #{assignment}
41
+ end
42
+ RUBY
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,30 +1,47 @@
1
- require 'anima'
2
-
3
1
  require 'rom/struct'
4
2
 
3
+ require 'rom/support/cache'
4
+ require 'rom/support/constants'
5
+ require 'rom/support/class_builder'
6
+
7
+ require 'rom/repository/struct_attributes'
8
+
5
9
  module ROM
6
10
  class Repository
7
11
  # @api private
8
12
  class StructBuilder
9
- attr_reader :registry
13
+ extend Cache
14
+
15
+ def call(*args)
16
+ fetch_or_store(*args) do
17
+ name, header = args
18
+
19
+ build_class(name) { |klass|
20
+ klass.send(:include, StructAttributes.new(visit(header)))
21
+ }
22
+ end
23
+ end
24
+ alias_method :[], :call
25
+
26
+ private
10
27
 
11
- def self.registry
12
- @__registry__ ||= {}
28
+ def visit(ast)
29
+ name, node = ast
30
+ __send__("visit_#{name}", node)
13
31
  end
14
32
 
15
- def initialize
16
- @registry = self.class.registry
33
+ def visit_header(node)
34
+ node.map(&method(:visit))
17
35
  end
18
36
 
19
- def call(*args)
20
- name, columns = args
21
- registry[args.hash] ||= build_class(name) { |klass|
22
- klass.send(:include, Anima.new(*columns))
23
- }
37
+ def visit_relation(node)
38
+ relation_name, meta, * = node
39
+ meta[:combine_name] || relation_name.relation
24
40
  end
25
- alias_method :[], :call
26
41
 
27
- private
42
+ def visit_attribute(node)
43
+ node
44
+ end
28
45
 
29
46
  def build_class(name, &block)
30
47
  ROM::ClassBuilder.new(name: class_name(name), parent: Struct).call(&block)
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  class Repository
3
- VERSION = '0.2.0'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
5
5
  end
@@ -1,60 +1,221 @@
1
1
  require 'rom/support/deprecations'
2
2
  require 'rom/support/options'
3
3
 
4
+ require 'rom/repository/class_interface'
4
5
  require 'rom/repository/mapper_builder'
5
- require 'rom/repository/loading_proxy'
6
+ require 'rom/repository/relation_proxy'
7
+ require 'rom/repository/command_compiler'
8
+
9
+ require 'rom/repository/root'
10
+ require 'rom/repository/changeset'
6
11
 
7
12
  module ROM
13
+ # Abstract repository class to inherit from
14
+ #
15
+ # A repository provides access to composable relations and commands. Its job is
16
+ # to provide application-specific data that is already materialized, so that
17
+ # relations don't leak into your application layer.
18
+ #
19
+ # Typically, you're going to work with Repository::Root that are configured to
20
+ # use a single relation as its root, and compose aggregates and use commands
21
+ # against the root relation.
22
+ #
23
+ # @example
24
+ # class MyRepo < ROM::Repository[:users]
25
+ # relations :users, :tasks
26
+ #
27
+ # def users_with_tasks
28
+ # users.combine_children(tasks: tasks).to_a
29
+ # end
30
+ # end
31
+ #
32
+ # rom = ROM.container(:sql, 'sqlite::memory') do |conf|
33
+ # conf.default.create_table(:users) do
34
+ # primary_key :id
35
+ # column :name, String
36
+ # end
37
+ #
38
+ # conf.default.create_table(:tasks) do
39
+ # primary_key :id
40
+ # column :user_id, Integer
41
+ # column :title, String
42
+ # end
43
+ # end
44
+ #
45
+ # my_repo = MyRepo.new(rom)
46
+ # my_repo.users_with_tasks
47
+ #
48
+ # @see Repository::Root
49
+ #
50
+ # @api public
8
51
  class Repository
9
- # Abstract repository class to inherit from
52
+ # @deprecated
53
+ class Base < Repository
54
+ def self.inherited(klass)
55
+ super
56
+ Deprecations.announce(self, 'inherit from Repository instead')
57
+ end
58
+ end
59
+
60
+ extend ClassInterface
61
+
62
+ # @!attribute [r] container
63
+ # @return [ROM::Container] The container used to set up a repo
64
+ attr_reader :container
65
+
66
+ # @!attribute [r] relations
67
+ # @return [RelationRegistry] The relation proxy registry used by a repo
68
+ attr_reader :relations
69
+
70
+ # @!attribute [r] mappers
71
+ # @return [MapperBuilder] The auto-generated mappers for repo relations
72
+ attr_reader :mappers
73
+
74
+ # Initializes a new repo by establishing configured relation proxies from
75
+ # the passed container
10
76
  #
11
- # TODO: rename this to Repository once deprecated Repository from rom core is gone
77
+ # @param container [ROM::Container] The rom container with relations and optional commands
12
78
  #
13
79
  # @api public
14
- include Options
80
+ def initialize(container)
81
+ @container = container
82
+ @mappers = MapperBuilder.new
83
+ @relations = RelationRegistry.new do |registry, relations|
84
+ self.class.relations.each do |name|
85
+ relation = container.relation(name)
15
86
 
16
- option :mapper_builder, reader: true, default: proc { MapperBuilder.new }
87
+ proxy = RelationProxy.new(
88
+ relation, name: name, mappers: mappers, registry: registry
89
+ )
90
+
91
+ instance_variable_set("@#{name}", proxy)
92
+
93
+ relations[name] = proxy
94
+ end
95
+ end
96
+ end
17
97
 
18
- # Define which relations your repository is going to use
98
+ # @overload command(type, relation)
99
+ # Returns a command for a relation
100
+ #
101
+ # @example
102
+ # repo.command(:create, repo.users)
103
+ #
104
+ # @param type [Symbol] The command type (:create, :update or :delete)
105
+ # @param relation [RelationProxy] The relation for which command should be built for
106
+ #
107
+ # @overload command(options)
108
+ # Builds a command for a given relation identifier
109
+ #
110
+ # @example
111
+ # repo.command(create: :users)
112
+ #
113
+ # @param options [Hash<Symbol=>Symbol>] A type => rel_name map
114
+ #
115
+ # @overload command(rel_name)
116
+ # Returns command registry for a given relation identifier
117
+ #
118
+ # @example
119
+ # repo.command(:users)[:my_custom_command]
19
120
  #
20
- # @example
21
- # class MyRepo < ROM::Repository::Base
22
- # relations :users, :tasks
23
- # end
121
+ # @param rel_name [Symbol] The relation identifier from the container
24
122
  #
25
- # my_repo = MyRepo.new(rom_env)
123
+ # @return [CommandRegistry]
26
124
  #
27
- # my_repo.users
28
- # my_repo.tasks
125
+ # @overload command(rel_name, &block)
126
+ # Yields a command graph composer for a given relation identifier
29
127
  #
30
- # @return [Array<Symbol>]
128
+ # @param rel_name [Symbol] The relation identifier from the container
129
+ #
130
+ # @return [ROM::Command]
31
131
  #
32
132
  # @api public
33
- def self.relations(*names)
34
- if names.any?
35
- attr_reader(*names)
36
- @relations = names
133
+ def command(*args, **opts, &block)
134
+ all_args = args + opts.to_a.flatten
135
+
136
+ if all_args.size > 1
137
+ commands.fetch_or_store(all_args.hash) do
138
+ compile_command(*args, **opts)
139
+ end
37
140
  else
38
- @relations
141
+ container.command(*args, &block)
39
142
  end
40
143
  end
41
144
 
42
- # @api private
43
- def initialize(env, options = {})
44
- super
45
- self.class.relations.each do |name|
46
- proxy = LoadingProxy.new(
47
- env.relation(name), name: name, mapper_builder: mapper_builder
48
- )
49
- instance_variable_set("@#{name}", proxy)
145
+ # @overload changeset(name, attributes)
146
+ # Returns a create changeset for a given relation identifier
147
+ #
148
+ # @example
149
+ # repo.changeset(:users, name: "Jane")
150
+ #
151
+ # @param name [Symbol] The relation container identifier
152
+ # @param attributes [Hash]
153
+ #
154
+ # @return [Changeset::Create]
155
+ #
156
+ # @overload changeset(name, restriction_arg, attributes)
157
+ # Returns an update changeset for a given relation identifier
158
+ #
159
+ # @example
160
+ # repo.changeset(:users, 1, name: "Jane Doe")
161
+ #
162
+ # @param name [Symbol] The relation container identifier
163
+ # @param restriction_arg [Object] The argument passed to restricted view
164
+ #
165
+ # @return [Changeset::Update]
166
+ #
167
+ # @api public
168
+ def changeset(*args)
169
+ if args.size == 2
170
+ name, data = args
171
+ elsif args.size == 3
172
+ name, pk, data = args
173
+ else
174
+ raise ArgumentError, 'Repository#changeset accepts 2 or 3 arguments'
175
+ end
176
+
177
+ relation = relations[name]
178
+
179
+ if pk
180
+ Changeset::Update.new(relation, data, primary_key: pk)
181
+ else
182
+ Changeset::Create.new(relation, data)
50
183
  end
51
184
  end
52
185
 
53
- class Base < Repository
54
- def self.inherited(klass)
55
- super
56
- Deprecations.announce(self, 'inherit from Repository instead')
186
+ private
187
+
188
+ # Local command cache
189
+ #
190
+ # @api private
191
+ def commands
192
+ @__commands__ ||= Concurrent::Map.new
193
+ end
194
+
195
+ # Build a new command or return existing one
196
+ #
197
+ # @api private
198
+ def compile_command(*args, mapper: nil, use: nil, **opts)
199
+ type, name = args + opts.to_a.flatten(1)
200
+
201
+ relation = name.is_a?(Symbol) ? relations[name] : name
202
+
203
+ ast = relation.to_ast
204
+ adapter = relations[relation.name].adapter
205
+
206
+ if mapper
207
+ mapper_instance = container.mappers[relation.name.relation][mapper]
208
+ else
209
+ mapper_instance = mappers[ast]
57
210
  end
211
+
212
+ command = CommandCompiler[container, type, adapter, ast, use]
213
+ command >> mapper_instance
214
+ end
215
+
216
+ # @api private
217
+ def map_tuple(relation, tuple)
218
+ relations[relation.name].mapper.([tuple]).first
58
219
  end
59
220
  end
60
221
  end
data/lib/rom/struct.rb CHANGED
@@ -1,7 +1,3 @@
1
- require 'anima'
2
-
3
- require 'rom/support/class_builder'
4
-
5
1
  module ROM
6
2
  # Simple data-struct
7
3
  #
@@ -9,7 +5,7 @@ module ROM
9
5
  #
10
6
  # @api public
11
7
  class Struct
12
- # Coerce to hash
8
+ # Coerces a struct to a hash
13
9
  #
14
10
  # @return [Hash]
15
11
  #
@@ -18,15 +14,24 @@ module ROM
18
14
  to_h
19
15
  end
20
16
 
21
- # Access attribute value
17
+ # Reads an attribute value
22
18
  #
23
- # @param [Symbol] name The name of the attribute
19
+ # @param name [Symbol] The name of the attribute
24
20
  #
25
21
  # @return [Object]
26
22
  #
27
23
  # @api public
28
24
  def [](name)
29
- instance_variable_get("@#{name}")
25
+ __send__(name)
26
+ end
27
+
28
+ # Returns a short string representation
29
+ #
30
+ # @return [String]
31
+ #
32
+ # @api public
33
+ def to_s
34
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
30
35
  end
31
36
  end
32
37
  end
@@ -4,22 +4,21 @@ require File.expand_path('../lib/rom/repository/version', __FILE__)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = 'rom-repository'
7
- gem.summary = 'Repository for ROM with auto-mapping and relation extensions'
8
- gem.description = gem.summary
7
+ gem.summary = 'Repository abstraction for rom-rb'
8
+ gem.description = 'rom-repository adds support for auto-mapping and commands on top of rom-rb relations'
9
9
  gem.author = 'Piotr Solnica'
10
- gem.email = 'piotr.solnica@gmail.com'
10
+ gem.email = 'piotr.solnica+oss@gmail.com'
11
11
  gem.homepage = 'http://rom-rb.org'
12
12
  gem.require_paths = ['lib']
13
13
  gem.version = ROM::Repository::VERSION.dup
14
- gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') }
14
+ gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') || name.include?('examples') || name.include?('bin') }
15
15
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
16
16
  gem.license = 'MIT'
17
17
 
18
- gem.add_runtime_dependency 'anima', '~> 0.2', '>= 0.2'
19
- gem.add_runtime_dependency 'rom', '~> 1.0.0'
20
- gem.add_runtime_dependency 'rom-support', '~> 1.0.0'
21
- gem.add_runtime_dependency 'rom-mapper', '~> 0.3.0'
18
+ gem.add_runtime_dependency 'rom', '~> 2.0'
19
+ gem.add_runtime_dependency 'rom-support', '~> 2.0'
20
+ gem.add_runtime_dependency 'rom-mapper', '~> 0.4'
22
21
 
23
- gem.add_development_dependency 'rake', '~> 10.3'
24
- gem.add_development_dependency 'rspec', '~> 3.3'
22
+ gem.add_development_dependency 'rake', '~> 11.2'
23
+ gem.add_development_dependency 'rspec', '~> 3.5'
25
24
  end
@@ -0,0 +1,86 @@
1
+ RSpec.describe 'Using changesets' do
2
+ include_context 'database'
3
+ include_context 'relations'
4
+
5
+ describe 'Create' do
6
+ subject(:repo) do
7
+ Class.new(ROM::Repository[:users]) {
8
+ relations :books, :posts
9
+ commands :create, update: :by_pk
10
+ }.new(rom)
11
+ end
12
+
13
+ it 'can be passed to a command' do
14
+ changeset = repo.changeset(name: "Jane Doe")
15
+ command = repo.command(:create, repo.users)
16
+ result = command.(changeset)
17
+
18
+ expect(result.id).to_not be(nil)
19
+ expect(result.name).to eql("Jane Doe")
20
+ end
21
+
22
+ it 'can be passed to a command graph' do
23
+ changeset = repo.changeset(
24
+ name: "Jane Doe", posts: [{ title: "Just Do It", alien: "or sutin" }]
25
+ )
26
+
27
+ command = repo.command(:create, repo.aggregate(:posts))
28
+ result = command.(changeset)
29
+
30
+ expect(result.id).to_not be(nil)
31
+ expect(result.name).to eql("Jane Doe")
32
+ expect(result.posts.size).to be(1)
33
+ expect(result.posts[0].title).to eql("Just Do It")
34
+ end
35
+
36
+ it 'preprocesses data using changeset pipes' do
37
+ changeset = repo.changeset(:books, title: "rom-rb is awesome").map(:add_timestamps)
38
+ command = repo.command(:create, repo.books)
39
+ result = command.(changeset)
40
+
41
+ expect(result.id).to_not be(nil)
42
+ expect(result.title).to eql("rom-rb is awesome")
43
+ expect(result.created_at).to be_instance_of(Time)
44
+ expect(result.updated_at).to be_instance_of(Time)
45
+ end
46
+ end
47
+
48
+ describe 'Update' do
49
+ subject(:repo) do
50
+ Class.new(ROM::Repository[:books]) {
51
+ commands :create, update: :by_pk
52
+ }.new(rom)
53
+ end
54
+
55
+ it 'can be passed to a command' do
56
+ book = repo.create(title: 'rom-rb is awesome')
57
+
58
+ changeset = repo
59
+ .changeset(book.id, title: 'rom-rb is awesome for real')
60
+ .map(:touch)
61
+
62
+ expect(changeset.diff).to eql(title: 'rom-rb is awesome for real')
63
+
64
+ result = repo.update(book.id, changeset)
65
+
66
+ expect(result.id).to be(book.id)
67
+ expect(result.title).to eql('rom-rb is awesome for real')
68
+ expect(result.updated_at).to be_instance_of(Time)
69
+ end
70
+
71
+ it 'skips update execution with no diff' do
72
+ book = repo.create(title: 'rom-rb is awesome')
73
+
74
+ changeset = repo
75
+ .changeset(book.id, title: 'rom-rb is awesome')
76
+
77
+ expect(changeset).to_not be_diff
78
+
79
+ result = repo.update(book.id, changeset)
80
+
81
+ expect(result.id).to be(book.id)
82
+ expect(result.title).to eql('rom-rb is awesome')
83
+ expect(result.updated_at).to be(nil)
84
+ end
85
+ end
86
+ end