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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +12 -0
- data/fdp_client_ruby.gemspec +33 -0
- data/lib/fdp_client/GUI/access_service.rb +53 -0
- data/lib/fdp_client/GUI/catalog.rb +56 -0
- data/lib/fdp_client/GUI/dataset.rb +59 -0
- data/lib/fdp_client/GUI/distribution.rb +55 -0
- data/lib/fdp_client/GUI/resource.rb +302 -0
- data/lib/fdp_client/GUI/version.rb +7 -0
- data/lib/fdp_client/fdp_client.rb +223 -0
- data/lib/fdp_client/fdp_resource.rb +366 -0
- data/lib/fdp_client/fdp_schema.rb +293 -0
- data/lib/fdp_client.rb +10 -0
- metadata +62 -0
|
@@ -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
|