fdp_client 0.0.2

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.
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rest-client" # assumed already required upstream
4
+ require "json"
5
+
6
+ module FDP
7
+ ##
8
+ # Represents a resource definition in a FAIR Data Point server.
9
+ #
10
+ # Resource definitions describe types of metadata records that can be registered
11
+ # in the FDP (e.g. Dataset, Data Service, Repository, or custom types like Biobank).
12
+ # Each resource definition specifies:
13
+ #
14
+ # - which metadata schemas (SHACL shapes) apply,
15
+ # - target RDF classes (usually dcat:Resource or subclasses),
16
+ # - hierarchical children (nested resource types),
17
+ # - external links/properties shown in the UI.
18
+ #
19
+ # Resource names must be unique. New resources are created via POST to
20
+ # +/resource-definitions+, existing ones updated via PUT to
21
+ # +/resource-definitions/{uuid}+.
22
+ #
23
+ # @see FDP::Client#retrieve_current_resources
24
+ # @see FDP::Client#list_current_resources
25
+ # @see FDP::Schema for the metadata shapes applied to these resources
26
+ #
27
+ class Resource
28
+ # @return [FDP::Client] Client used for API operations
29
+ attr_accessor :client
30
+
31
+ # @return [String, nil] UUID of the resource definition (nil if new)
32
+ attr_accessor :uuid
33
+
34
+ # @return [String] Unique internal name (no spaces/special chars recommended)
35
+ attr_accessor :name
36
+
37
+ # @return [String, nil] URL prefix suggestion for generated resource URLs
38
+ attr_accessor :prefix
39
+
40
+ # @return [Array<String>] UUIDs of metadata schemas (SHACL shapes) that apply
41
+ attr_accessor :schemas
42
+
43
+ # @return [Array<String>] Target RDF class URIs this resource represents
44
+ attr_accessor :targeturis
45
+
46
+ # @return [Array<FDP::ResourceChild>] Nested/child resource definitions
47
+ attr_accessor :children
48
+
49
+ # @return [Array<FDP::ResourceExternalLink>] External links shown in UI
50
+ attr_accessor :external_links
51
+
52
+ # @return [String] Human-readable description
53
+ attr_accessor :description
54
+
55
+ ##
56
+ # Initializes a new Resource definition.
57
+ #
58
+ # @overload initialize(client:, resourcejson:)
59
+ # Create from existing API response (parsed JSON)
60
+ # @param resourcejson [Hash] Parsed JSON from /resource-definitions endpoint
61
+ #
62
+ # @overload initialize(client:, name:, schemas:, ...)
63
+ # Create a new resource definition (to be written to server)
64
+ # @param name [String] Unique name (checked if uuid nil)
65
+ # @param schemas [Array<String>] Schema UUIDs
66
+ # @param description [String, nil]
67
+ # @param prefix [String, nil]
68
+ # @param targeturis [Array<String>]
69
+ # @param children [Array<FDP::ResourceChild>]
70
+ # @param external_links [Array<FDP::ResourceExternalLink>]
71
+ # @param uuid [String, nil] Existing UUID
72
+ #
73
+ # @raise [ArgumentError] if name already exists on server (when creating new)
74
+ #
75
+ def initialize(client:, resourcejson: nil, schemas: [], description: nil, prefix: nil,
76
+ targeturis: [], children: [], external_links: [], uuid: nil, name: nil)
77
+ @client = client
78
+ @uuid = uuid
79
+ @name = name
80
+ @description = description
81
+ @prefix = prefix
82
+ @targeturis = targeturis
83
+ @children = children
84
+ @external_links = external_links
85
+
86
+ if resourcejson
87
+ # Hydrate from existing server response
88
+ @uuid = resourcejson["uuid"]
89
+ @name = resourcejson["name"]
90
+ @prefix = resourcejson["urlPrefix"]
91
+ @schemas = resourcejson["metadataSchemaUuids"] || []
92
+ @targeturis = resourcejson["targetClassUris"] || []
93
+ @description = resourcejson["description"] || "No description provided"
94
+
95
+ @children = (resourcejson["children"] || []).map do |childjson|
96
+ ResourceChild.new(childjson: childjson)
97
+ end
98
+
99
+ @external_links = (resourcejson["externalLinks"] || []).map do |linkjson|
100
+ ResourceExternalLink.new(linkjson: linkjson)
101
+ end
102
+ else
103
+ # New resource — enforce name uniqueness
104
+ @schemas = schemas
105
+ @description = description || "No description provided"
106
+ validate_name(name: name) unless uuid
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Validates that the resource name does not already exist on the server.
112
+ #
113
+ # @param name [String]
114
+ #
115
+ # @raise [ArgumentError] if name is taken
116
+ #
117
+ def validate_name(name:)
118
+ return unless @client.list_current_resources.key?(name)
119
+
120
+ raise ArgumentError,
121
+ "Resource name '#{name}' already exists on the server. " \
122
+ "Update the existing resource rather than creating a duplicate."
123
+ end
124
+
125
+ ##
126
+ # Builds the payload suitable for POST/PUT to /resource-definitions.
127
+ #
128
+ # Removes duplicate children/links based on their #key.
129
+ #
130
+ # @return [Hash] API-ready payload with string keys
131
+ #
132
+ def to_api_payload
133
+ # warn "CHILDreN vbefore #{children.inspect}"
134
+ # warn "Links vbefore #{external_links.inspect}"
135
+ if children&.first
136
+ grouped = children.group_by { |c| c.key } # ← explicit block, proven to work
137
+ children = grouped.values.map(&:first)
138
+ end
139
+ # same for external_links
140
+ if external_links&.first
141
+ grouped = external_links.group_by { |c| c.key }
142
+ external_links = grouped.values.map(&:first)
143
+ end
144
+ children = [] if children.nil?
145
+ external_links = [] if external_links.nil?
146
+ # warn "CHILDreN after #{children.inspect}"
147
+ # warn "Links after #{external_links.inspect}"
148
+
149
+ payload = {
150
+ uuid: uuid,
151
+ name: name,
152
+ urlPrefix: prefix,
153
+ metadataSchemaUuids: schemas,
154
+ targetClassUris: targeturis,
155
+ children: children.map(&:to_api_payload),
156
+ externalLinks: external_links.map(&:to_api_payload),
157
+ description: description
158
+ }
159
+
160
+ payload.transform_keys(&:to_s)
161
+ end
162
+
163
+ ##
164
+ # Writes this resource definition to the FDP server (create or update).
165
+ #
166
+ # @param client [FDP::Client] (defaults to @client)
167
+ #
168
+ # @return [self, nil] self on success, nil on failure
169
+ #
170
+ def write_to_fdp(client: @client)
171
+ if uuid.to_s.strip.empty?
172
+ write_new_resource(client: client)
173
+ else
174
+ replace_existing_resource(client: client)
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ ##
181
+ # Creates a new resource definition via POST.
182
+ #
183
+ # @param client [FDP::Client]
184
+ #
185
+ # @return [self, nil]
186
+ #
187
+ def write_new_resource(client:)
188
+ payload = to_api_payload
189
+ payload.delete("uuid") # server generates UUID
190
+
191
+ warn "Creating new resource definition '#{name}'"
192
+
193
+ begin
194
+ response = RestClient.post(
195
+ "#{client.base_url}/resource-definitions",
196
+ payload.to_json,
197
+ client.headers
198
+ )
199
+
200
+ result = JSON.parse(response.body)
201
+ # warn "Resource '#{name}' created successfully with UUID: #{result["uuid"]}"
202
+ self.uuid = result["uuid"]
203
+ self
204
+ rescue RestClient::ExceptionWithResponse => e
205
+ warn "Create failed for resource '#{name}' (HTTP #{e.response.code}):"
206
+ warn e.response.body
207
+ nil
208
+ rescue StandardError => e
209
+ warn "Unexpected error creating resource '#{name}': #{e.message}"
210
+ nil
211
+ end
212
+ end
213
+
214
+ ##
215
+ # Replaces (updates) an existing resource definition via PUT.
216
+ #
217
+ # @param client [FDP::Client]
218
+ #
219
+ # @return [self, nil]
220
+ #
221
+ def replace_existing_resource(client:)
222
+ payload = to_api_payload
223
+
224
+ warn "Replacing resource definition '#{name}' (UUID: #{uuid})"
225
+
226
+ begin
227
+ RestClient.put(
228
+ "#{client.base_url}/resource-definitions/#{uuid}",
229
+ payload.to_json,
230
+ client.headers
231
+ )
232
+
233
+ # warn "Resource '#{name}' updated successfully."
234
+ self
235
+ rescue RestClient::ExceptionWithResponse => e
236
+ warn "Update failed for resource '#{name}' (HTTP #{e.response.code}):"
237
+ warn e.response.body
238
+ nil
239
+ rescue StandardError => e
240
+ warn "Unexpected error updating resource '#{name}': #{e.message}"
241
+ nil
242
+ end
243
+ end
244
+ end
245
+
246
+ ##
247
+ # Represents a child resource reference within a parent {FDP::Resource}.
248
+ #
249
+ # Defines a nested/hierarchical relationship (e.g. a Dataset inside a Repository).
250
+ #
251
+ class ResourceChild
252
+ # @return [String] UUID of the child resource definition
253
+ attr_accessor :resourceDefinitionUuid
254
+
255
+ # @return [String] RDF property URI expressing the parent → child relation
256
+ attr_accessor :relationUri
257
+
258
+ # @return [String] Title shown in list views
259
+ attr_accessor :listViewTitle
260
+
261
+ # @return [String, nil] URI for tags displayed in list views
262
+ attr_accessor :listViewTagsUri
263
+
264
+ # @return [Array] Metadata fields shown in list views
265
+ attr_accessor :listViewMetadata
266
+
267
+ ##
268
+ # @overload initialize(childjson:)
269
+ # From existing API response
270
+ # @overload initialize(resourceDefinitionUuid:, relationUri:, ...)
271
+ # New child definition
272
+ #
273
+ # @raise [ArgumentError] if required fields missing in new mode
274
+ #
275
+ def initialize(childjson: nil, resourceDefinitionUuid: nil, relationUri: nil,
276
+ listViewTitle: nil, listViewTagsUri: nil, listViewMetadata: [])
277
+ if childjson
278
+ @resourceDefinitionUuid = childjson["resourceDefinitionUuid"]
279
+ @relationUri = childjson["relationUri"]
280
+ list_view = childjson["listView"] || {}
281
+ @listViewTitle = list_view["title"] || "No title provided"
282
+ @listViewTagsUri = list_view["tagsUri"]
283
+ @listViewMetadata = list_view["metadata"] || []
284
+ else
285
+ raise ArgumentError, "resourceDefinitionUuid and relationUri are required" \
286
+ unless resourceDefinitionUuid && relationUri
287
+
288
+ @resourceDefinitionUuid = resourceDefinitionUuid
289
+ @relationUri = relationUri
290
+ @listViewTitle = listViewTitle
291
+ @listViewTagsUri = listViewTagsUri
292
+ @listViewMetadata = listViewMetadata
293
+ end
294
+ end
295
+
296
+ ##
297
+ # Unique key used for de-duplication within a parent resource.
298
+ #
299
+ # @return [Array<String>]
300
+ #
301
+ def key
302
+ [resourceDefinitionUuid, relationUri]
303
+ end
304
+
305
+ ##
306
+ # @return [Hash] API-ready payload
307
+ #
308
+ def to_api_payload
309
+ {
310
+ resourceDefinitionUuid: resourceDefinitionUuid,
311
+ relationUri: relationUri,
312
+ listView: {
313
+ title: listViewTitle,
314
+ tagsUri: listViewTagsUri,
315
+ metadata: listViewMetadata
316
+ }
317
+ }.transform_keys(&:to_s)
318
+ end
319
+ end
320
+
321
+ ##
322
+ # Represents an external link / property displayed for a resource in the FDP UI.
323
+ #
324
+ class ResourceExternalLink
325
+ # @return [String] Display title of the link
326
+ attr_accessor :title
327
+
328
+ # @return [String] RDF property URI this link represents
329
+ attr_accessor :propertyuri
330
+
331
+ ##
332
+ # @overload initialize(linkjson:)
333
+ # From API response
334
+ # @overload initialize(title:, propertyuri:)
335
+ # New external link
336
+ #
337
+ def initialize(linkjson: nil, title: nil, propertyuri: nil)
338
+ if linkjson
339
+ @title = linkjson["title"]
340
+ @propertyuri = linkjson["propertyUri"]
341
+ else
342
+ @title = title
343
+ @propertyuri = propertyuri
344
+ end
345
+ end
346
+
347
+ ##
348
+ # Unique key for de-duplication.
349
+ #
350
+ # @return [Array<String>]
351
+ #
352
+ def key
353
+ [propertyuri, title]
354
+ end
355
+
356
+ ##
357
+ # @return [Hash] API-ready payload
358
+ #
359
+ def to_api_payload
360
+ {
361
+ title: title,
362
+ propertyUri: propertyuri
363
+ }.transform_keys(&:to_s)
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rest-client" # assuming already required in client or elsewhere
4
+ require "json"
5
+
6
+ module FDP
7
+ ##
8
+ # Represents a metadata schema (SHACL shape) in a FAIR Data Point server.
9
+ #
10
+ # This class models a custom or extended metadata shape used for validating
11
+ # resource metadata records in an FDP instance. Schemas can extend others
12
+ # (inheritance via parents), target specific classes (usually subclasses of
13
+ # dcat:Resource), and contain the raw Turtle SHACL definition.
14
+ #
15
+ # == Key concepts
16
+ #
17
+ # - Schemas are identified by a unique +name+ (internal identifier).
18
+ # - The FDP enforces uniqueness on schema names — you cannot create a new
19
+ # schema with an existing name; you must update the existing one.
20
+ # - Schemas support versioning; updates typically increment the patch version.
21
+ # - New schemas are created via POST to +/metadata-schemas+.
22
+ # - Existing schemas are updated via PUT to +/metadata-schemas/{uuid}/draft+,
23
+ # followed by publishing via POST to +/metadata-schemas/{uuid}/versions+.
24
+ #
25
+ # @see FDP::Client for fetching and writing schemas
26
+ #
27
+ class Schema
28
+ # @return [FDP::Client] The client used to interact with the FDP server
29
+ attr_accessor :client
30
+
31
+ # @return [String, nil] UUID of the schema (nil for new/unpersisted schemas)
32
+ attr_accessor :uuid
33
+
34
+ # @return [String] Unique internal name (no spaces/special chars recommended)
35
+ attr_accessor :name
36
+
37
+ # @return [String] Human-readable label (suggested resource name in UI)
38
+ attr_accessor :label
39
+
40
+ # @return [String] Description of the schema
41
+ attr_accessor :description
42
+
43
+ # @return [String, nil] Suggested URL prefix for resources using this shape
44
+ attr_accessor :prefix
45
+
46
+ # @return [String] Raw Turtle SHACL definition string
47
+ attr_accessor :definition
48
+
49
+ # @return [String] Version string (SemVer-like, e.g. "1.0.0")
50
+ attr_accessor :version
51
+
52
+ # @return [Array<String>] UUIDs of parent schemas this one extends
53
+ attr_accessor :parents
54
+
55
+ # @return [Array<String>] UUIDs of child schemas that extend this one
56
+ attr_accessor :children
57
+
58
+ # @return [Array<String>] Target class URIs (must include dcat:Resource)
59
+ attr_accessor :targetclasses
60
+
61
+ # @return [Boolean] Whether this is an abstract schema (not directly assignable)
62
+ attr_accessor :abstractschema
63
+
64
+ ##
65
+ # Initializes a new Schema object.
66
+ #
67
+ # @param client [FDP::Client] Required client for API operations
68
+ # @param name [String] Unique schema name (checked against server if uuid nil)
69
+ # @param label [String] Display name
70
+ # @param description [String] Schema description
71
+ # @param definition [String] Turtle SHACL content
72
+ # @param prefix [String, nil] Optional URL prefix suggestion
73
+ # @param version [String] Version (default "1.0.0" suggested)
74
+ # @param targetclasses [Array<String>] Target class URIs
75
+ # @param parents [Array<String>] Parent schema UUIDs (default [])
76
+ # @param children [Array<String>] Child schema UUIDs (default [])
77
+ # @param uuid [String, nil] Existing UUID (nil for new schema)
78
+ # @param abstractschema [Boolean] Abstract flag (default false)
79
+ #
80
+ # @raise [ArgumentError] if name already exists on server (when creating new)
81
+ #
82
+ def initialize(client:, name:, label:, description:, definition:, prefix:, version:,
83
+ targetclasses:, parents: [], children: [], uuid: nil, abstractschema: false)
84
+ @client = client
85
+ @uuid = uuid
86
+ @name = name
87
+ @label = label
88
+ @description = description
89
+ @prefix = prefix
90
+ @definition = definition
91
+ @version = version
92
+ @parents = parents
93
+ @children = children
94
+ @targetclasses = ["http://www.w3.org/ns/dcat#Resource", *targetclasses].uniq
95
+ @abstractschema = abstractschema
96
+
97
+ # Enforce name uniqueness when creating a new schema
98
+ validate_name(name: name) unless uuid
99
+ end
100
+
101
+ ##
102
+ # Validates that the schema name does not already exist on the server.
103
+ #
104
+ # @param name [String] Name to check
105
+ #
106
+ # @raise [ArgumentError] if name is already taken
107
+ #
108
+ def validate_name(name:)
109
+ return unless @client.list_current_schemas.key?(name)
110
+
111
+ raise ArgumentError,
112
+ "Schema name '#{name}' already exists on the server. " \
113
+ "You must update the existing schema rather than create a duplicate."
114
+ end
115
+
116
+ ##
117
+ # Builds the payload suitable for POST/PUT to the FDP /metadata-schemas endpoint.
118
+ #
119
+ # @return [Hash] API-ready payload with string keys
120
+ #
121
+ def to_api_payload
122
+ @children.uniq!
123
+ @parents.uniq!
124
+
125
+ payload = {
126
+ uuid: uuid, # nil for create → server generates
127
+ name: name,
128
+ description: description,
129
+ abstractSchema: abstractschema,
130
+ suggestedResourceName: label,
131
+ suggestedUrlPrefix: prefix,
132
+ published: true, # we publish immediately after
133
+ definition: definition,
134
+ extendsSchemaUuids: parents,
135
+ version: version,
136
+ targetClasses: targetclasses,
137
+ childSchemaUuids: children
138
+ }
139
+
140
+ # Ensure string keys (some API versions may be strict)
141
+ payload.transform_keys(&:to_s)
142
+ end
143
+
144
+ ##
145
+ # Writes this schema to the FDP server (create if new, overwrite if existing).
146
+ #
147
+ # Automatically handles create vs. update, version increment, and publishing.
148
+ #
149
+ # @param client [FDP::Client] Client to use (usually same as +@client+)
150
+ #
151
+ # @return [String, nil] Response body on success, nil on failure
152
+ #
153
+ def write_to_fdp(client: @client)
154
+ # warn "Current UUID for schema '#{name}': #{uuid || "(new)"}"
155
+
156
+ if uuid.to_s.strip.empty?
157
+ write_new_schema_to_fdp(client: client)
158
+ else
159
+ warn "Schema '#{name}' has UUID #{uuid}. Overwriting with new definition."
160
+ warn "Change name or clear uuid if you want to create a new version instead."
161
+ overwrite_schema_in_fdp(client: client)
162
+ end
163
+ end
164
+
165
+ ##
166
+ # Increments the patch version (adds 1, or custom logic).
167
+ #
168
+ # Currently adds +5+ to patch — consider changing to +1+ for standard SemVer.
169
+ #
170
+ # @return [String] New version string
171
+ #
172
+ def increment_version
173
+ # warn "Incrementing patch version for schema '#{name}' (current: #{version})"
174
+ major, minor, patch = version.split(".").map(&:to_i)
175
+ patch += 1
176
+ new_version = "#{major}.#{minor}.#{patch}"
177
+ warn "New version: #{new_version}"
178
+ new_version
179
+ end
180
+
181
+ private
182
+
183
+ ##
184
+ # Overwrites an existing schema (PUT to draft, then publish).
185
+ #
186
+ # @param client [FDP::Client]
187
+ #
188
+ # @return [String, nil]
189
+ #
190
+ def overwrite_schema_in_fdp(client:)
191
+ payload = to_api_payload
192
+ new_version = increment_version
193
+ payload["version"] = new_version
194
+ self.version = new_version # local update (API may ignore client-supplied version)
195
+
196
+ warn "\nOVERWRITING schema '#{name}' (UUID: #{uuid}) with:\n#{JSON.pretty_generate(payload)}\n"
197
+
198
+ begin
199
+ response = RestClient.put(
200
+ "#{client.base_url}/metadata-schemas/#{uuid}/draft",
201
+ payload.to_json,
202
+ client.headers
203
+ )
204
+
205
+ result = JSON.parse(response.body)
206
+ # warn "Schema '#{name}' updated as draft (version #{version})"
207
+ self.uuid = result["uuid"] || uuid # usually unchanged
208
+
209
+ publish(client: client)
210
+ rescue RestClient::ExceptionWithResponse => e
211
+ warn "Overwrite failed for '#{name}' (HTTP #{e.response.code}):"
212
+ warn e.response.body
213
+ nil
214
+ rescue StandardError => e
215
+ warn "Unexpected error overwriting '#{name}': #{e.message}"
216
+ nil
217
+ end
218
+ end
219
+
220
+ ##
221
+ # Creates a new schema on the server, then publishes it.
222
+ #
223
+ # @param client [FDP::Client]
224
+ #
225
+ # @return [String, nil]
226
+ #
227
+ def write_new_schema_to_fdp(client:)
228
+ payload = to_api_payload
229
+ payload.delete("uuid") # server generates UUID
230
+
231
+ warn "\nCREATING new schema '#{name}' with:\n#{JSON.pretty_generate(payload)}\n"
232
+
233
+ begin
234
+ response = RestClient.post(
235
+ "#{client.base_url}/metadata-schemas",
236
+ payload.to_json,
237
+ client.headers
238
+ )
239
+
240
+ result = JSON.parse(response.body)
241
+ # warn "Schema '#{name}' created with UUID: #{result["uuid"]}"
242
+ self.uuid = result["uuid"]
243
+
244
+ publish(client: client)
245
+ rescue RestClient::ExceptionWithResponse => e
246
+ warn "Create failed for '#{name}' (HTTP #{e.response.code}):"
247
+ warn e.response.body
248
+ nil
249
+ rescue StandardError => e
250
+ warn "Unexpected error creating '#{name}': #{e.message}"
251
+ nil
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Publishes the current schema version (moves out of draft).
257
+ #
258
+ # Note: The FDP API requires +published: false+ in this payload to actually publish.
259
+ # This is counter-intuitive but matches observed reference implementation behavior.
260
+ #
261
+ # @param client [FDP::Client]
262
+ #
263
+ # @return [String, nil] Response body or nil
264
+ #
265
+ def publish(client:)
266
+ publish_payload = {
267
+ description: description,
268
+ version: version,
269
+ published: false # ← critical: API uses false here to trigger publish
270
+ }.to_json
271
+
272
+ # warn "\nPublishing schema '#{name}' (UUID: #{uuid}) with:\n#{JSON.pretty_generate(JSON.parse(publish_payload))}\n"
273
+
274
+ begin
275
+ response = RestClient.post(
276
+ "#{client.base_url}/metadata-schemas/#{uuid}/versions",
277
+ publish_payload,
278
+ client.headers
279
+ )
280
+
281
+ # warn "Schema '#{name}' published successfully."
282
+ response.body
283
+ rescue RestClient::ExceptionWithResponse => e
284
+ warn "Publish failed for '#{name}' (HTTP #{e.response.code}):"
285
+ warn e.response.body
286
+ nil
287
+ rescue StandardError => e
288
+ warn "Unexpected error publishing '#{name}': #{e.message}"
289
+ nil
290
+ end
291
+ end
292
+ end
293
+ end
data/lib/fdp_client.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fdp_client/fdp_client"
4
+ require_relative "fdp_client/fdp_resource"
5
+ require_relative "fdp_client/fdp_schema"
6
+
7
+ module FDP
8
+ class Error < StandardError; end
9
+
10
+ end