graphql_preload_queries 0.1.0 → 0.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e487e7903d2626761868cfce9a2aa368eeb8efbfa7a930eb00988b80c0ed331
4
- data.tar.gz: cbdff09086c7530c1a0015c3294ed897bb21fd5621fb46a62acb452543d503db
3
+ metadata.gz: 89226168aa382eb59c56754b96a4eaa64c51c5f06fc6fd8d831b7795d84adf0e
4
+ data.tar.gz: b29ec85aa619a1f7412944d6f50388f68c9b196cc5e8607bc22fd163f3a64ed1
5
5
  SHA512:
6
- metadata.gz: 2ced151a4c528db0b2bb29f5be1d8fe6aa4d316344510503682dccb1937760b92953ac1b2825e01d5397c751f618967a85bb9f518951a7127f0525f882412517
7
- data.tar.gz: b8c4a168788b2288955e941e3b5a806a125de2fe52c19fdf58956fba84c30c80e3e7e6e43e79fb285e38e7dbba590616b767d4617a7ec1869ca102d8708e41cb
6
+ metadata.gz: 42df9aacf5b59d341fc5498a61890a1ecd5c76713179c94b81696084af7d9d4f52abdd105c26a04775aa6b8199f86398841bf230127df711ffceef386752f7f9
7
+ data.tar.gz: cded97146bc935064f7dcaa94034deffd06adf1b6cf3c2daaacf746fb11eef542b261debd505ccb8963ec3b67f60cf7386d4829aeaa13feaed024786fa7ec04e
@@ -0,0 +1,20 @@
1
+ ## 0.3.1 (22-01-2021)
2
+ - feat: auto camelize key for queries and mutations
3
+
4
+ ## 0.3 (22-01-2021)
5
+ - feat: add debug mode
6
+ ```GraphqlPreloadQueries::DEBUG = true```
7
+ - fix: detect the correct Query Result Type
8
+
9
+ ## 0.2.2 (21-01-2021)
10
+ - Fix: Fix deep recursive stack error
11
+
12
+ ## 0.2.1 (21-01-2021)
13
+ - fix: add default preload to key
14
+ - fix: fix invalid key when deep preloading
15
+
16
+ ## 0.2.0 (02-12-2020)
17
+ - Refactor: Preload associations when iterating activeRecord::Relation
18
+
19
+ ## 0.1.0 (10-11-2020
20
+ - Add rails query preload support for queries, mutations and gql object types.
data/Gemfile CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ruby '2.6.5'
3
4
  source 'https://rubygems.org'
4
5
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
6
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql_preload_queries (0.1.0)
4
+ graphql_preload_queries (0.3.1)
5
5
  graphql
6
6
  rails
7
7
 
@@ -190,5 +190,8 @@ DEPENDENCIES
190
190
  rubocop-rspec
191
191
  sqlite3
192
192
 
193
+ RUBY VERSION
194
+ ruby 2.6.5p114
195
+
193
196
  BUNDLED WITH
194
197
  2.1.4
data/README.md CHANGED
@@ -1,77 +1,72 @@
1
- # GraphqlPreloadQueries (In progress)
2
- This gem helps to define all possible preloads for graphql data results and avoid the common problem "N+1 Queries".
1
+ # GraphqlPreloadQueries
2
+ This gem helps you to define all nested preloads to be added when required for graphql data results and avoid the common problem "N+1 Queries".
3
3
 
4
4
  ## Usage
5
- * Preloads in query results
5
+ * Object Type
6
6
  ```ruby
7
- # queries/articles.rb
8
- def articles
9
- resolve_preloads(Article.all, { allComments: :comments })
10
- end
7
+ class UserType < Types::BaseObject
8
+ add_preload 'parents|allParents', { preload: :parents, friends: :friends, parents: :parents }
9
+ add_preload :friends, { parents: { preload: :parents, parents: :parents, friends: :friends } }
10
+
11
+ field :id, Int, null: true
12
+ field :name, String, null: true
13
+ field :friends, [Types::UserType], null: false
14
+ field :parents, [Types::UserType], null: false
15
+ end
11
16
  ```
12
- When articles query is performed and:
13
- * The query includes "allComments", then ```:comments``` will automatically be preloaded
14
- * The query does not include "allComments", then ```:comments``` is not preloaded
17
+ Examples:
18
+ * ```add_preload(:friends)```
19
+ ```:friends``` association will be preloaded if query includes ```friends```, like: ```user(id: 10) { friends { ... } }```
15
20
 
16
- * Preloads in mutation results
21
+ * ```add_preload(:allFriends, :friends)```
22
+ ```:friends``` association will be preloaded if query includes ```allFriends```, like: ```user(id: 10) { allFriends { ... } }```
23
+
24
+ * ```add_preload(:allFriends, { preload: :friends, parents: :parents })```
25
+ ```:preload``` key can be used to indicate the association name when defining nested preloads, like: ```user(id: 10) { allFriends { id parents { ... } } }```
26
+
27
+ * ```add_preload(:friends, { allParents: :parents })```
28
+ (Nested 1 lvl preloading) ```friends: :parents``` association will be preloaded if query includes ```allParents```, like: ```user(id: 10) { friends { allParents { ... } } }```
29
+
30
+ * ```add_preload(:friends, { allParents: { preload: :parents, friends: :friends } })```
31
+ (Nested 2 levels preloading) ```friends: { parents: :friends }``` association will be preloaded if query includes ```friends``` inside ```parents```, like: ```user(id: 10) { friends { allParents { { friends { ... } } } } }```
32
+
33
+ * ```add_preload('friends|allFriends', :friends)```
34
+ (Multiple gql queries) ```:friends``` association will be preloaded if query includes ```friends``` or ```allFriends```, like: ```user(id: 10) { friends { ... } }``` OR ```user(id: 10) { allFriends { ... } }```
35
+
36
+ * ```add_preload('ignoredFriends', 'ignored_friends.user')```
37
+ (Deep preloading) ```{ ignored_friends: :user }``` association will be preloaded if query includes ```inogredFriends```, like: ```user(id: 10) { ignoredFriends { ... } }```
38
+
39
+ * Preloads in query results
17
40
  ```ruby
18
- # mutations/articles/approve.rb
19
- def resolve
20
- affected_articles = Article.where(id: [1,2,3])
21
- res = resolve_preloads(affected_articles, { allComments: :comments })
22
- { articles => res }
41
+ # queries/users.rb
42
+ def user(id:)
43
+ # includes all preloads defined in user type
44
+ # Sample: user(id: 10){ friends { id } }
45
+ # :friends will be preloaded inside "user" sql query
46
+ user = include_gql_preloads(:user, User.where(id: id))
47
+
48
+ # does not include user type preloads (only sub query preloads will be applied)
49
+ # Sample: user(id: 10){ friends { id parents { ... } } }
50
+ # Only :parents will be preloaded inside "friends" sql query
51
+ user = User.find(id)
23
52
  end
24
53
  ```
25
- When approve mutation is performed and:
26
- * The result articles query includes "allComments", then ```:comments``` will automatically be preloaded
27
- * The result articles query does not include "allComments", then ```:comments``` is not preloaded
54
+ - include_gql_preloads: Will preload all preloads configured in UserType based on the gql query.
28
55
 
29
- * Preloads in ObjectTypes
56
+ * Preloads in mutation results
30
57
  ```ruby
31
- # types/article_type.rb
32
- module Types
33
- class ArticleType < Types::BaseObject
34
- preload_field :allComments, [Types::CommentType], preload: { owner: :author }, null: false
35
- end
58
+ # mutations/users/disable.rb
59
+ #...
60
+ field :users, [Types::UserType], null: true
61
+ def resolve(ids:)
62
+ affected_users = User.where(id: ids)
63
+ affected_users = include_gql_preloads(:users, affected_users)
64
+ puts affected_users.first&.friends # will print preloaded friends data
65
+ { users: affected_users }
36
66
  end
37
67
  ```
38
- When any query is retrieving an article data and:
39
- * The query includes ```owner``` inside ```allComments```, then ```:author``` will automatically be preloaded inside "allComments" query
40
- * The query does not include ```owner```, then ```:author``` is not preloaded
41
- This field is exactly the same as the graphql field, except that this field expects for "preload" setting which contains all configurations for preloading
42
-
43
- Complex preload settings
44
- ```ruby
45
- # category query
46
- {
47
- 'posts' =>
48
- [:posts, # :posts preload key will be used when: { posts { id ... } }
49
- {
50
- 'authors|allAuthors' => [:author, { # :author key will be used when: { posts { allAuthors { id ... } } }
51
- address: :address # :address key will be used when: { posts { allAuthors { address { id ... } } } }
52
- }],
53
- history: :versions # :versions key will be used when: { posts { history { ... } } }
54
- }
55
- ],
56
- 'disabledPosts' => ['category_disabled_posts.post', { # :category_disabled_posts.post key will be used when: { disabledPosts { ... } }
57
- authors: :authors # :authors key will be used when: { disabledPosts { authors { ... } } }
58
- }]
59
- }
60
- ```
61
- * ```authors|allAuthors``` means that the preload will be added if "authors" or "allAuthors" is present in the query
62
- * ```category_disabled_posts.post``` means an inner preload, sample: ```posts.preload({ category_disabled_posts: :post })```
63
-
64
- ### Important:
65
- Is needed to omit "extra" params auto provided by Graphql when using custom resolver (only in case not using params), sample:
66
- ```ruby
67
- # types/post_type.rb
68
- preload_field :allComments, [Types::CommentType], preload: { owner: :author }, null: false
69
- def allComments(_omit_gql_params) # custom method resolver that omits non used params
70
- object.allComments
71
- end
72
- ```
68
+ - include_gql_preloads: Will preload all preloads configured in UserType based on the gql query.
73
69
 
74
-
75
70
  ## Installation
76
71
  Add this line to your application's Gemfile:
77
72
 
@@ -89,6 +84,12 @@ Or install it yourself as:
89
84
  $ gem install graphql_preload_queries
90
85
  ```
91
86
 
87
+ For debugging mode:
88
+ ```
89
+ # config/initializers/gql_preload.rb
90
+ GraphqlPreloadQueries::DEBUG = true
91
+ ```
92
+
92
93
  ## Contributing
93
94
  Bug reports and pull requests are welcome on GitHub at https://github.com/owen2345/graphql_preload_queries. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
94
95
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # preload resolver for mutations
4
+ Rails.application.config.to_prepare do
5
+ GraphQL::Schema::Mutation.class_eval do
6
+ # TODO: auto recover type_klass using result key
7
+ # Add corresponding preloads to mutation results
8
+ # @param gql_result_key (String | Sym)
9
+ # @param collection (ActiveCollection)
10
+ # @param type_klass (GQL TypeClass)
11
+ def include_gql_preloads(gql_result_key, collection, type_klass = nil)
12
+ gql_result_key = GraphQL::Schema::Member::BuildType.camelize(gql_result_key.to_s)
13
+ type_klass ||= preload_type_klass(gql_result_key.to_s)
14
+ klass = GraphqlPreloadQueries::Extensions::Preload
15
+ ast_node = preload_find_node(gql_result_key)
16
+ klass.preload_associations(collection, ast_node, type_klass)
17
+ end
18
+
19
+ private
20
+
21
+ def preload_find_node(key)
22
+ main_node = context.query.document.definitions.first.selections.first
23
+ main_node.selections.find { |node_i| node_i.name == key.to_s }
24
+ end
25
+
26
+ # @param result_key: String
27
+ def preload_type_klass(result_key)
28
+ res = self.class.fields[result_key].instance_variable_get(:@return_type_expr)
29
+ res.is_a?(Array) ? res.first : res
30
+ end
31
+ end
32
+ end
@@ -3,26 +3,34 @@
3
3
  require 'graphql_preload_queries/extensions/preload'
4
4
 
5
5
  Rails.application.config.to_prepare do
6
- # Custom preload field for Object types
7
6
  Types::BaseObject.class_eval do
8
- # @param key field[:key]
9
- # @param type field[:type]
10
- # @param settings field[:settings] ++ { preload: {} }
11
- # preload: (Hash) { allPosts: [:posts, { author: :author }] }
12
- # ==> <cat1>.preload(posts: :author) // if author and posts are in query
13
- # ==> <cat1>.preload(:posts) // if only author is in the query
14
- # ==> <cat1>.preload() // if both of them are not in the query
15
- # TODO: ability to merge extensions + extras
16
- def self.preload_field(key, type, settings = {})
17
- klass = GraphqlPreloadQueries::Extensions::Preload
18
- custom_attrs = {
19
- extras: [:ast_node],
20
- extensions: [klass => settings.delete(:preload)]
21
- }
22
- field key, type, settings.merge(custom_attrs)
7
+ class << self
8
+ def preloads
9
+ @preloads ||= {}
10
+ end
23
11
 
24
- # Fix: omit non expected "extras" param auto provided by graphql
25
- define_method(key) { |_omit_non_used_args| object.send(key) } unless method_defined? key
12
+ # @param key (Symbol|String)
13
+ # @param preload (Symbol|String or Symbol|String|Hash)
14
+ # @Sample:
15
+ ## key argument supports for multiple query names
16
+ # add_preload('users|allUsers', :users)
17
+ ## preload argument indicates the association name to be preloaded
18
+ # add_preload(:allUsers, :users)
19
+ ## preload argument supports for nested associations
20
+ # add_preload(:inactiveUsers, 'inactivated_users.user')
21
+ ## "preload" key should be specified to indicate the association name
22
+ # add_preload(:allUsers, { preload: :users, 'allComments|comments' => :comments } })
23
+ ## preload key can be omitted to use the same name as the key
24
+ # add_preload(:users, { 'allComments|comments' => :comments } })
25
+ def add_preload(key, preload = key)
26
+ preload ||= key
27
+ raise('Invalid preload query key') if [String, Symbol].exclude?(key.class)
28
+ raise('Invalid preload preload key') if [String, Symbol, Hash].exclude?(preload.class)
29
+
30
+ preload[:preload] ||= key if preload.is_a?(Hash)
31
+ key = GraphQL::Schema::Member::BuildType.camelize(key.to_s)
32
+ preloads[key] = preload
33
+ end
26
34
  end
27
35
  end
28
36
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # preload resolver for queries
4
+ Rails.application.config.to_prepare do
5
+ Types::QueryType.class_eval do
6
+ # TODO: auto recover type_klass using result key
7
+ # Add corresponding preloads to query results
8
+ # Note: key is automatically calculated based on method name
9
+ # @param gql_result_key (String | Sym)
10
+ # @param collection (ActiveCollection)
11
+ # @param type_klass (GQL TypeClass, default: calculates using return type)
12
+ def include_gql_preloads(gql_result_key, collection, type_klass = nil)
13
+ gql_result_key = GraphQL::Schema::Member::BuildType.camelize(gql_result_key.to_s)
14
+ type_klass ||= preload_type_klass(gql_result_key.to_s)
15
+ klass = GraphqlPreloadQueries::Extensions::Preload
16
+ ast_node = preload_find_node(gql_result_key)
17
+ klass.preload_associations(collection, ast_node, type_klass)
18
+ end
19
+
20
+ private
21
+
22
+ # @param key: Symbol
23
+ def preload_find_node(key)
24
+ main_node = context.query.document.definitions.first
25
+ main_node.selections.find { |node_i| node_i.name == key.to_s }
26
+ end
27
+
28
+ # @param result_key: String
29
+ def preload_type_klass(result_key)
30
+ res = self.class.fields[result_key].instance_variable_get(:@return_type_expr)
31
+ res.is_a?(Array) ? res.first : res
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/execution/interpreter/runtime'
4
+ module GraphqlPreloadQueries::PatchContinueValue # rubocop:disable Style/ClassAndModuleChildren:
5
+ # gql args: path, value, parent_type, field, is_non_null, ast_node
6
+ def continue_value(*args)
7
+ value = args[1]
8
+ ast_node = args[5]
9
+ field = args[3]
10
+ type_klass = Array(field.instance_variable_get(:@return_type_expr))[0]
11
+ is_active_record = value.is_a?(ActiveRecord::Relation)
12
+ return super if !is_active_record || value.loaded? || !type_klass.respond_to?(:preloads)
13
+
14
+ klass = GraphqlPreloadQueries::Extensions::Preload
15
+ klass.preload_associations(value, ast_node, type_klass)
16
+ end
17
+ end
18
+ GraphQL::Execution::Interpreter::Runtime.prepend GraphqlPreloadQueries::PatchContinueValue
@@ -5,4 +5,8 @@ require 'graphql'
5
5
  require 'graphql_preload_queries/extensions/preload'
6
6
 
7
7
  module GraphqlPreloadQueries
8
+ DEBUG = false
9
+ def self.log(msg)
10
+ puts "***GraphqlPreloadQueries: #{msg}" if DEBUG
11
+ end
8
12
  end
@@ -5,51 +5,54 @@
5
5
  module GraphqlPreloadQueries
6
6
  module Extensions
7
7
  class Preload < GraphQL::Schema::FieldExtension
8
- # extension to add eager loading when a field was already processed
9
- def resolve(object:, arguments:, **_rest)
10
- klass = GraphqlPreloadQueries::Extensions::Preload
11
- res = yield(object, arguments)
12
- return res unless res
13
-
14
- klass.resolve_preloads(res, arguments[:ast_node], (options || {}))
15
- end
16
-
17
8
  class << self
18
9
  # Add all the corresponding preloads to the collection
19
- # @param data (ActiveCollection)
20
- # @return @data with preloads configured
21
- # Sample: resolve_preloads(Category.all, { allPosts: :posts })
22
- def resolve_preloads(data, query_node, preload_config)
23
- apply_preloads(data, filter_preloads(query_node, preload_config))
10
+ # @param value (ActiveCollection)
11
+ # @param @node (GqlNode)
12
+ # @param @type_klass (GqlTypeKlass)
13
+ # @return @data with necessary preloads
14
+ def preload_associations(value, node, type_klass)
15
+ preloads = filter_preloads(node, type_klass.preloads || {})
16
+ log_info = { type_klass: type_klass, preloads: preloads, configured: type_klass.preloads }
17
+ GraphqlPreloadQueries.log("Preloading: #{log_info}")
18
+ apply_preloads(value, preloads)
24
19
  end
25
20
 
21
+ private
22
+
26
23
  def apply_preloads(collection, preloads)
27
24
  collection.eager_load(preloads)
28
25
  end
29
26
 
30
27
  # find all configured preloads inside a node
31
28
  def filter_preloads(node, preload_conf, root = nested_hash)
29
+ return root unless node
30
+
32
31
  preload_conf.map do |key, sub_preload_conf|
33
32
  filter_preload(node, key, sub_preload_conf, root)
34
33
  end
35
34
  root
36
35
  end
37
36
 
38
- private
39
-
40
37
  # find preloads under a specific key
41
38
  def filter_preload(node, key, preload_conf, root)
42
- sub_node = node.selections.find do |node_i|
43
- key.to_s.split('|').include?(node_i.name.to_s)
44
- end
45
-
46
- multiple_preload = preload_conf.is_a?(Array)
39
+ sub_node = sub_node(node, key)
40
+ multiple_preload = preload_conf.is_a?(Hash)
47
41
  return unless sub_node
48
- return add_preload_key(root, preload_conf, []) unless multiple_preload
42
+ return add_preload_key(root, preload_conf, {}) unless multiple_preload
49
43
 
50
44
  child_root = nested_hash
51
- filter_preloads(sub_node, preload_conf[1], child_root)
52
- add_preload_key(root, preload_conf[0], child_root.presence || [])
45
+ association_name = preload_conf[:preload] || key.to_s.underscore
46
+ filter_preloads(sub_node, preload_conf, child_root)
47
+ add_preload_key(root, association_name, child_root.presence || {})
48
+ end
49
+
50
+ def sub_node(node, key)
51
+ is_relay_node = %w[nodes edges].include?(node.selections.first.name)
52
+ node = node.selections.first if is_relay_node
53
+ node.selections.find do |node_i|
54
+ key.to_s.split('|').include?(node_i.name.to_s)
55
+ end
53
56
  end
54
57
 
55
58
  # parse nested preload key and add it to the tree
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlPreloadQueries
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_preload_queries
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - owen2345
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-27 00:00:00.000000000 Z
11
+ date: 2021-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -120,15 +120,17 @@ files:
120
120
  - ".gitignore"
121
121
  - ".rspec"
122
122
  - ".rubocop.yml"
123
+ - CHANGELOG.md
123
124
  - Gemfile
124
125
  - Gemfile.lock
125
126
  - MIT-LICENSE
126
127
  - README.md
127
128
  - Rakefile
128
129
  - bin/rails
129
- - config/initializers/add_mutation_resolver.rb
130
+ - config/initializers/add_mutation_helper.rb
130
131
  - config/initializers/add_preload_field.rb
131
- - config/initializers/add_query_resolver.rb
132
+ - config/initializers/add_query_helper.rb
133
+ - config/initializers/patch_continue_value.rb
132
134
  - gemfiles/Gemfile_4
133
135
  - gemfiles/Gemfile_5
134
136
  - gemfiles/Gemfile_6
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # preload resolver for mutations
4
- Rails.application.config.to_prepare do
5
- GraphQL::Schema::Mutation.class_eval do
6
- # Add corresponding preloads to mutation results
7
- # @param key (sym) key of the query
8
- # @param data (ActiveCollection)
9
- # @param preload_config (Same as Field: field[:preload])
10
- def resolve_preloads(key, data, preload_config)
11
- node = find_node(key)
12
- return data unless node
13
-
14
- # relay support (TODO: add support to skip when not using relay)
15
- node = node.selections.first if %w[nodes edges].include?(node.selections.first.name)
16
- GraphqlPreloadQueries::Extensions::Preload.resolve_preloads(data, node, preload_config)
17
- end
18
-
19
- private
20
-
21
- def find_node(key)
22
- main_node = context.query.document.definitions.first.selections.first
23
- main_node.selections.find { |node_i| node_i.name == key.to_s }
24
- end
25
- end
26
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # preload resolver for queries
4
- Rails.application.config.to_prepare do
5
- Types::QueryType.class_eval do
6
- # Add corresponding preloads to query results
7
- # Note: key is automatically calculated based on method name
8
- # @param data (ActiveCollection)
9
- # @param preload_config (Same as Field: field[:preload])
10
- def resolve_preloads(data, preload_config)
11
- node = find_node(caller[0][/`.*'/][1..-2])
12
- return data unless node
13
-
14
- # relay support (TODO: add support to skip when not using relay)
15
- node = node.selections.first if %w[nodes edges].include?(node.selections.first.name)
16
- GraphqlPreloadQueries::Extensions::Preload.resolve_preloads(data, node, preload_config)
17
- end
18
-
19
- private
20
-
21
- def find_node(key)
22
- main_node = context.query.document.definitions.first
23
- main_node.selections.find { |node_i| node_i.name == key.to_s }
24
- end
25
- end
26
- end