sequel_mapper 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CODE_OF_CONDUCT.md +28 -0
  4. data/Gemfile.lock +32 -2
  5. data/MissingFeatures.md +64 -0
  6. data/README.md +141 -72
  7. data/Rakefile +29 -0
  8. data/TODO.md +16 -11
  9. data/features/env.rb +57 -0
  10. data/features/example.feature +121 -0
  11. data/features/step_definitions/example_steps.rb +46 -0
  12. data/lib/sequel_mapper.rb +6 -2
  13. data/lib/sequel_mapper/abstract_record.rb +53 -0
  14. data/lib/sequel_mapper/association_loaders.rb +52 -0
  15. data/lib/sequel_mapper/collection_mutability_proxy.rb +77 -0
  16. data/lib/sequel_mapper/configurations/conventional_association_configuration.rb +187 -0
  17. data/lib/sequel_mapper/configurations/conventional_configuration.rb +269 -0
  18. data/lib/sequel_mapper/dataset.rb +37 -0
  19. data/lib/sequel_mapper/deleted_record.rb +16 -0
  20. data/lib/sequel_mapper/dirty_map.rb +31 -0
  21. data/lib/sequel_mapper/graph_loader.rb +48 -0
  22. data/lib/sequel_mapper/graph_serializer.rb +107 -0
  23. data/lib/sequel_mapper/identity_map.rb +22 -0
  24. data/lib/sequel_mapper/lazy_object_proxy.rb +51 -0
  25. data/lib/sequel_mapper/many_to_many_association.rb +181 -0
  26. data/lib/sequel_mapper/many_to_one_association.rb +60 -0
  27. data/lib/sequel_mapper/mapper_facade.rb +180 -0
  28. data/lib/sequel_mapper/one_to_many_association.rb +51 -0
  29. data/lib/sequel_mapper/public_conveniencies.rb +27 -0
  30. data/lib/sequel_mapper/query_order.rb +32 -0
  31. data/lib/sequel_mapper/queryable_lazy_dataset_loader.rb +70 -0
  32. data/lib/sequel_mapper/relation_mapping.rb +35 -0
  33. data/lib/sequel_mapper/serializer.rb +18 -0
  34. data/lib/sequel_mapper/short_inspection_string.rb +18 -0
  35. data/lib/sequel_mapper/subset_queries_proxy.rb +11 -0
  36. data/lib/sequel_mapper/upserted_record.rb +15 -0
  37. data/lib/sequel_mapper/version.rb +1 -1
  38. data/sequel_mapper.gemspec +3 -0
  39. data/spec/config_override_spec.rb +167 -0
  40. data/spec/custom_serializers_spec.rb +77 -0
  41. data/spec/deletion_spec.rb +104 -0
  42. data/spec/graph_persistence_spec.rb +83 -88
  43. data/spec/graph_traversal_spec.rb +32 -31
  44. data/spec/new_graph_persistence_spec.rb +69 -0
  45. data/spec/object_identity_spec.rb +70 -0
  46. data/spec/ordered_association_spec.rb +46 -16
  47. data/spec/persistence_efficiency_spec.rb +186 -0
  48. data/spec/predefined_queries_spec.rb +73 -0
  49. data/spec/proxying_spec.rb +25 -19
  50. data/spec/querying_spec.rb +24 -27
  51. data/spec/readme_examples_spec.rb +35 -0
  52. data/spec/sequel_mapper/abstract_record_spec.rb +179 -0
  53. data/spec/sequel_mapper/{association_proxy_spec.rb → collection_mutability_proxy_spec.rb} +6 -6
  54. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  55. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  56. data/spec/sequel_mapper/public_conveniencies_spec.rb +49 -0
  57. data/spec/sequel_mapper/queryable_lazy_dataset_loader_spec.rb +103 -0
  58. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  59. data/spec/spec_helper.rb +7 -10
  60. data/spec/support/blog_schema.rb +29 -0
  61. data/spec/support/have_persisted_matcher.rb +19 -0
  62. data/spec/support/mapper_setup.rb +234 -0
  63. data/spec/support/mock_sequel.rb +0 -1
  64. data/spec/support/object_graph_setup.rb +106 -0
  65. data/spec/support/seed_data_setup.rb +122 -0
  66. data/spec/support/sequel_persistence_setup.rb +19 -0
  67. data/spec/support/sequel_test_support.rb +159 -0
  68. metadata +121 -15
  69. data/lib/sequel_mapper/association_proxy.rb +0 -54
  70. data/lib/sequel_mapper/belongs_to_association_proxy.rb +0 -27
  71. data/lib/sequel_mapper/graph.rb +0 -174
  72. data/lib/sequel_mapper/queryable_association_proxy.rb +0 -23
  73. data/spec/sequel_mapper/belongs_to_association_proxy_spec.rb +0 -65
  74. data/spec/support/graph_fixture.rb +0 -331
  75. data/spec/support/query_counter.rb +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2a26f63ff570a4dace02b8407237d137e91fcc86
4
- data.tar.gz: 682421f11a6baccefbe7969510f345d17ea26d71
3
+ metadata.gz: dfb105e407d34250b83618b2c789687d0fe1f86f
4
+ data.tar.gz: 01aeb7f356ab17224b3c93f10fbf8b8204258c98
5
5
  SHA512:
6
- metadata.gz: ebc65d4a2915be9f85d269c417f47d96bec45255601a8a389b88c5bdc009420fac2c9f65ad64c8cdb13fea33a877b3d8db0274264e6f3242cd2cff3f207a6111
7
- data.tar.gz: 6742b37bb795d34aeadc8fd765bc76976cafcf0e5c1b24873dc5720d1631b07cca1391c3914774acb4b8026514830ea13dcd9bcb7a35d2cb762042fc712898b3
6
+ metadata.gz: 24b33c4a55324d01e8a37321e7a19e94ff7a4f275f0960f16fda3d63691a2fba5ee8c51f6da1a3ea4eeea2c03457e9eae7470da1e48267f3ee102711d73d882a
7
+ data.tar.gz: 127f9594d9409b4b09cd7c20517d7f55e2368655f06ce713de238e59af2247ede9dd0b07ae627e5876e2581e47814724693c1f1cf799735005b049a0182848bf
@@ -1 +1 @@
1
- 2.1.3
1
+ 2.2.3
@@ -0,0 +1,28 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all
4
+ people who contribute through reporting issues, posting feature requests,
5
+ updating documentation, submitting pull requests or patches, and other
6
+ activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, age, or religion.
12
+
13
+ Examples of unacceptable behavior by participants include the use of sexual
14
+ language or imagery, derogatory comments or personal attacks, trolling, public
15
+ or private harassment, insults, or other unprofessional conduct.
16
+
17
+ Project maintainers have the right and responsibility to remove, edit, or
18
+ reject comments, commits, code, wiki edits, issues, and other contributions
19
+ that are not aligned to this Code of Conduct. Project maintainers who do not
20
+ follow the Code of Conduct may be removed from the project team.
21
+
22
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
23
+ reported by opening an issue or contacting one or more of the project
24
+ maintainers.
25
+
26
+ This Code of Conduct is adapted from the [Contributor
27
+ Covenant](http:contributor-covenant.org), version 1.0.0, available at
28
+ [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
@@ -1,15 +1,38 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sequel_mapper (0.0.1)
4
+ sequel_mapper (0.0.3)
5
+ activesupport (~> 4.0)
6
+ fetchable (~> 1.0)
5
7
  sequel (~> 4.16)
6
8
 
7
9
  GEM
8
10
  remote: https://rubygems.org/
9
11
  specs:
12
+ activesupport (4.2.4)
13
+ i18n (~> 0.7)
14
+ json (~> 1.7, >= 1.7.7)
15
+ minitest (~> 5.1)
16
+ thread_safe (~> 0.3, >= 0.3.4)
17
+ tzinfo (~> 1.1)
18
+ builder (3.2.2)
10
19
  coderay (1.1.0)
20
+ cucumber (1.3.20)
21
+ builder (>= 2.1.2)
22
+ diff-lcs (>= 1.1.3)
23
+ gherkin (~> 2.12)
24
+ multi_json (>= 1.7.5, < 2.0)
25
+ multi_test (>= 0.1.2)
11
26
  diff-lcs (1.2.5)
27
+ fetchable (1.0.0)
28
+ gherkin (2.12.2)
29
+ multi_json (~> 1.3)
30
+ i18n (0.7.0)
31
+ json (1.8.3)
12
32
  method_source (0.8.2)
33
+ minitest (5.8.3)
34
+ multi_json (1.11.1)
35
+ multi_test (0.1.2)
13
36
  pg (0.17.1)
14
37
  pry (0.10.1)
15
38
  coderay (~> 1.1.0)
@@ -28,16 +51,23 @@ GEM
28
51
  rspec-mocks (3.1.3)
29
52
  rspec-support (~> 3.1.0)
30
53
  rspec-support (3.1.2)
31
- sequel (4.16.0)
54
+ sequel (4.26.0)
32
55
  slop (3.6.0)
56
+ thread_safe (0.3.5)
57
+ tzinfo (1.2.2)
58
+ thread_safe (~> 0.1)
33
59
 
34
60
  PLATFORMS
35
61
  ruby
36
62
 
37
63
  DEPENDENCIES
38
64
  bundler (~> 1.7)
65
+ cucumber
39
66
  pg (~> 0.17.1)
40
67
  pry (~> 0.10.1)
41
68
  rake (~> 10.0)
42
69
  rspec (~> 3.1)
43
70
  sequel_mapper!
71
+
72
+ BUNDLED WITH
73
+ 1.10.6
@@ -0,0 +1,64 @@
1
+ # Missing features
2
+
3
+ The following features not included in SequelMapper are omitted purposefully to
4
+ keep the library simple and encourage good practices in application code.
5
+
6
+ Please open an issue if you feel like any of these features are essential or if
7
+ you think you can contribute to a solution please open an issue to discuss.
8
+
9
+ ## Coercion
10
+
11
+ Database supported types will be returned as expected, `Fixnum`, `DateTime`, `nil` etc.
12
+ Should you wish to enhance this data, every row is passed into the mapping's
13
+ factory function where you have the opportunity to do arbitrary transformations
14
+ before instantiating the domain object.
15
+
16
+ \*see note on transforming row data
17
+
18
+ ## Validation
19
+
20
+ This is the concern of your domain model and/or application boundaries.
21
+ SequelMapper allows you to persist any object you wish assuming schema
22
+ compatibility.
23
+
24
+ ## Database column name aliasing
25
+
26
+ While at first glance this is a simple feature, the abstraction starts to leak
27
+ when the using the query interface and guaranteeing all queries are substituted
28
+ perfectly is beyond the scope of the current version.
29
+
30
+ Should you wish to simply pass a column's key with a different parameter name
31
+ then you can again lean on the factory function to transform the row's data
32
+ before the domain object receives it.
33
+
34
+ \*see note on transforming row data
35
+
36
+ ## Cascade deletion
37
+
38
+ This is chiefly a data concern and is handled by a good database more
39
+ efficiently and effectively than any ORM could hope.
40
+
41
+ ## Database generated IDs and timestamps
42
+
43
+ While database generated values may work, available only after an object is
44
+ retrieved, they are not currently supported.
45
+
46
+ Data important to your domain should be generated in your application layer.
47
+ UUIDs make much more flexible identifiers for domain objects and further enable
48
+ decoupling and fast tests.
49
+
50
+ Timestamps are useful and important to most applications however if they are
51
+ used in your domain they should be pushed from explicitly from application
52
+ layer. You should again find this affords you more flexibility and decoupling.
53
+
54
+ There is absolutely nothing wrong with data added at time of persistence for
55
+ auditing purposes but SequelMapper will make you actively decide whether this
56
+ data should be available to the domain and what should be explicitly added.
57
+
58
+ \* Transforming row data
59
+
60
+ Adding a custom factory method to transform row data before passing it to the
61
+ domain layer is highly encouraged. However, ensure that for each custom factory
62
+ a serializer function is also supplied that SequelMapper can use to reverse the
63
+ operation for persistence.
64
+
data/README.md CHANGED
@@ -1,41 +1,62 @@
1
1
  # SequelMapper
2
2
 
3
- **Very new, much experimental, so incomplete**
3
+ ## TL;DR
4
4
 
5
- ## What it is
5
+ * A Ruby ORM that enables DDD and clean architectural styles.
6
+ * Persists plain objects while supporting arbitrarily deeply nested / circular associations
7
+ * Provides excellent database and query building support courtesy of [Sequel library](https://github.com/jeremyevans/sequel)
6
8
 
7
- SequelMapper is a data mapper that pulls rows out of your database and maps
8
- them into a graph of plain Ruby objects. The graph can then be modifed and
9
- persisted back into the database as a whole.
9
+ SequelMapper (working title) is a new, currently experimental [data mapper](http://martinfowler.com/eaaCatalog/dataMapper.html) ORM implementation for Ruby.
10
10
 
11
- The main feature is that it fully supports all the kinds of data associations
12
- that you are used to with ActiveRecord but for your POROs.
11
+ The aim is to provide a convenient way to query and persist graphs of Ruby objects (think models with associations), while keeping those object completely isolated and decoupled from the database.
13
12
 
14
- It is built on top of Jeremy Evans' Sequel library.
13
+ In contrast to Ruby's many [active record](http://martinfowler.com/eaaCatalog/activeRecord.html) implementations, domain objects require no special inherited or mixed in behavior in order to be persisted.
15
14
 
16
- ## Why is it?
15
+ ## Features
17
16
 
18
- * It seems like ROM may not be finished any time soon and I felt I could put
19
- together something functional albeit less ambitious
20
- * I love the Sequel library
21
- * I love decoupling persistence
22
- * Writing a complex datamapper is sure way to stall your project so I'm writing
23
- one for you
24
- * I am a sick person who enjoys this sort of thing
17
+ * Associations (belongs_to, has_many, has_many_through)
18
+ * Automatic 'convention over configuration' that is fully customizable
19
+ * Lazy loading for database read efficiency
20
+ * Dirty tracking for database write efficiency
21
+ * Predefined queries, scopes or subsets
22
+ * Eager loading to avoid the `n + 1` query problem
25
23
 
26
- So go on, persist those POROs, they don't even have to know about it.
24
+ There are some [conspicuous missing features](https://github.com/bestie/sequel_mapper/blob/master/MissingFeatures.md)
25
+ that you may want to read more about. If you want to contribute to solving any
26
+ of the problems listed please open an issue to discuss.
27
27
 
28
- ## Example
28
+ SequelMapper does not reinvent the wheel with querying abstraction and
29
+ migrations, instead these responsibilities are delegated to Sequel such that
30
+ its full power can be utilised.
31
+
32
+ For [querying](http://sequel.jeremyevans.net/rdoc/files/doc/querying_rdoc.html),
33
+ [migrations](http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html)
34
+ and creating your [database connection](http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html)
35
+ see the Sequel documentation.
36
+
37
+ ## Getting started
38
+
39
+ Please try this out, experiment, open issues and pull requests. Please read the
40
+ code of conduct first.
29
41
 
30
42
  ```ruby
31
- # Let's say you have some domain objects
43
+
44
+ # 1. Define some domain objects, structs will surfice for the example
32
45
 
33
46
  User = Struct.new(:id, :first_name, :last_name, :email, :posts)
34
- Post = Struct.new(:id, :author, :subject, :body, :comments, :categories)
35
- Comment = Struct.new(:id, :post, :commenter, :body)
47
+ Post = Struct.new(:id, :author, :subject, :body, :created_at, :categories)
36
48
  Category = Struct.new(:id, :name, :posts)
37
49
 
38
- # And a relational database with some tables that look similar
50
+ ## Also assume that a conventional database schema (think Rails) is in place,
51
+ ## a column for each of the struct's attributes will be present. The posts
52
+ ## table will have `author_id` as a foreign key to the users table. There is
53
+ ## a join table named `categories_to_posts` which facilitates the many to
54
+ ## many relationship.
55
+
56
+ # 2. Configure a Sequel database connection
57
+
58
+ ## Sequel Mapper does not manage your connection for you.
59
+ ## Example assumes Postgres however Sequel supports many other databases.
39
60
 
40
61
  DB = Sequel.postgres(
41
62
  host: ENV.fetch("PGHOST"),
@@ -43,70 +64,118 @@ So go on, persist those POROs, they don't even have to know about it.
43
64
  database: ENV.fetch("PGDATABASE"),
44
65
  )
45
66
 
46
- user_mapper = SequelMapper::Graph.new(
47
- top_level_namespace: :users,
67
+ # 3. Configure mappings and associations
68
+
69
+ ## This is kept separate from your domain models as knowledge of the schema
70
+ ## is required to wire them up.
71
+
72
+ USER_MAPPER_CONFIG = SequelMapper.config(DB)
73
+ .setup_mapping(:users) { |users|
74
+ users.has_many(:posts, foreign_key: :author_id)
75
+ }
76
+ .setup_mapping(:posts) { |posts|
77
+ posts.belongs_to(:author, mapping_name: :users)
78
+ posts.has_many_through(:categories)
79
+ }
80
+ .setup_mapping(:categories) { |categories|
81
+ categories.has_many_through(:posts)
82
+ }
83
+
84
+ # 4. Create a mapper by combining a connection and a configuration
85
+
86
+ USER_MAPPER = SequelMapper.mapper(
48
87
  datastore: DB,
49
- config: mapper_config, # Config omitted
88
+ config: USER_MAPPER_CONFIG,
89
+ name: :users,
50
90
  )
51
91
 
52
- # Then this may appeal to you
53
-
54
- user = user_mapper.where(id: 1).first
55
- # => [#<struct User
56
- # id=1,
57
- # first_name="Stephen",
58
- # last_name="Best",
59
- # email="bestie@gmail.com",
60
- # posts=#<SequelMapper::AssociationProxy:0x007ffbc3c7cb50 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>>]
61
-
62
- user.posts
63
- # => #<SequelMapper::AssociationProxy:0x007ffbc3c7cb50 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>
64
- # That's lazily evaluated try ...
65
-
66
- user.posts.to_a
67
- # => [#<struct Post
68
- # id=1,
69
- # author=
70
- # #<struct User
71
- # id=1,
72
- # first_name="Stephen",
73
- # last_name="Best",
74
- # email="bestie@gmail.com",
75
- # posts=#<SequelMapper::AssociationProxy:0x007ffbc3c7cb50 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>>,
76
- # subject="Object mapping",
77
- # body="It is often tricky",
78
- # comments=#<SequelMapper::AssociationProxy:0x007ffbc59377b8 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>,
79
- # categories=#<SequelMapper::AssociationProxy:0x007ffbc5936138 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>>,
80
- # #<struct Post
81
- # id=2,
82
- # author=
83
- # #<struct User
84
- # id=1,
85
- # first_name="Stephen",
86
- # last_name="Best",
87
- # email="bestie@gmail.com",
88
- # posts=#<SequelMapper::AssociationProxy:0x007ffbc3c7cb50 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>>,
89
- # subject="Object mapping part 2",
90
- # body="Lazy load all the things!",
91
- # comments=#<SequelMapper::AssociationProxy:0x007ffbc5935990 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>,
92
- # categories=#<SequelMapper::AssociationProxy:0x007ffbc592fe50 @assoc_enum=#<Enumerator::Lazy: ...>, @removed_nodes=[]>>]
93
-
94
- # And then access the comments and so on ...
92
+ ## You are not limted to one mapper configuration or one database connection.
93
+ ## To handle complex situations you may create several segregated mappings
94
+ ## for your separate aggregate roots, potentially utilising multiple
95
+ ## databases and different domain object classes/compositions.
96
+
97
+ # 5. Create some objects
98
+
99
+ user = User.new(
100
+ "2f0f791c-47cf-4a00-8676-e582075bcd65",
101
+ "Hansel",
102
+ "Trickett",
103
+ "hansel@tricketts.org",
104
+ [],
105
+ )
106
+
107
+ user.posts << Post.new(
108
+ "9b75fe2b-d694-4b90-9137-6201d426dda2",
109
+ user,
110
+ "Things that I like",
111
+ "I like fish and scratching",
112
+ Time.parse("2015-10-03 21:00:00 UTC"),
113
+ [],
114
+ )
115
+
116
+ # 6. Save them
117
+
118
+ USER_MAPPER.save(user)
119
+
120
+ ## Only the (aggregate) root object needs to be passed to the mapper.
121
+
122
+ # 7. Query
123
+
124
+ user = USER_MAPPER.where(id: "2f0f791c-47cf-4a00-8676-e582075bcd65").first
125
+
126
+ # => #<struct User
127
+ # id="2f0f791c-47cf-4a00-8676-e582075bcd65",
128
+ # first_name="Stephen",
129
+ # last_name="Best",
130
+ # email="bestie@gmail.com",
131
+ # posts=#<SequelMapper::CollectionMutabilityProxy:7ff57192d510 >,
132
+
133
+ ```
134
+
135
+ ## Running the tests
136
+
137
+ ### Set the following environment variables
138
+ * PGHOST
139
+ * PGUSER
140
+ * PGDATABASE
141
+
142
+ ### Create a test database
143
+
144
+ This will create a database named from the value of `PGDATABASE`
145
+
146
+ ```
147
+ $ bundle exec rake db:create
148
+ ```
149
+
150
+ ### Run all tests (RSpec and Cucumber)
151
+ ```
152
+ $ bundle exec rake
153
+ ```
154
+
155
+ ### Should anything go awry
156
+
157
+ Drop the test database and start fresh
158
+
159
+ ```
160
+ $ bundle exec rake db:drop
95
161
  ```
96
162
 
97
163
  ## Installation
98
164
 
99
- Add this line to your application's Gemfile:
165
+ This library is still pre 1.0 so please lock down your version and update with
166
+ care.
100
167
 
101
- ```ruby
102
- gem 'sequel_mapper'
168
+ Add the following to your `Gemfile`.
169
+
170
+ ```
171
+ gem "sequel_mapper", "0.0.3"
103
172
  ```
104
173
 
105
174
  And then execute:
106
175
 
107
176
  $ bundle
108
177
 
109
- Or install it yourself as:
178
+ Or install it manually:
110
179
 
111
180
  $ gem install sequel_mapper
112
181
 
data/Rakefile CHANGED
@@ -1,2 +1,31 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
+ require 'rspec/core/rake_task'
4
+ require 'cucumber/rake/task'
5
+
6
+ RSpec::Core::RakeTask.new
7
+ Cucumber::Rake::Task.new
8
+
9
+ task :default => [
10
+ :spec,
11
+ :cucumber,
12
+ ]
13
+
14
+ require_relative "spec/support/sequel_test_support"
15
+ require_relative "spec/support/blog_schema"
16
+
17
+ namespace :db do
18
+ include SequelMapper::SequelTestSupport
19
+
20
+ task :setup => [:create] do
21
+ create_tables(BLOG_SCHEMA)
22
+ end
23
+
24
+ task :create do
25
+ create_database
26
+ end
27
+
28
+ task :drop do
29
+ drop_database
30
+ end
31
+ end