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,302 @@
1
+ require "linkeddata"
2
+ require "rest-client"
3
+
4
+ module FDPMate
5
+ DCAT = RDF::Vocabulary.new("http://www.w3.org/ns/dcat#")
6
+ FOAF = RDF::Vocabulary.new("http://xmlns.com/foaf/0.1/")
7
+ BS = RDF::Vocabulary.new("http://rdf.biosemantics.org/ontologies/fdp-o#")
8
+
9
+ class DCATResource
10
+ attr_accessor :baseURI, :parentURI, :serverURL, :accessRights,
11
+ :conformsTo, :contactName, :contactEmail, :creator,
12
+ :creatorName, :title, :description, :issued, :modified,
13
+ :hasVersion, :publisher, :identifier, :license, :language,
14
+ :dataset, :keyword, :landingPage, :qualifiedRelation,
15
+ :theme, :service, :themeTaxonomy, :homepage, :types, :g # the graph
16
+
17
+ def initialize(types: [DCAT.Resource], baseURI: nil, parentURI: nil,
18
+ accessRights: nil, conformsTo: nil, contactEmail: nil, contactName: nil, creator: nil, creatorName: nil,
19
+ title: nil, description: nil, issued: nil, modified: nil, hasVersion: nil, publisher: nil,
20
+ identifier: nil, license: nil, language: "http://id.loc.gov/vocabulary/iso639-1/en",
21
+ dataset: nil, keyword: nil, landingPage: nil, qualifiedRelation: nil, theme: nil,
22
+ service: nil, themeTaxonomy: nil, homepage: nil, serverURL: "http://localhost:7070",
23
+ **_args)
24
+
25
+ @accessRights = accessRights
26
+ @conformsTo = conformsTo
27
+ @contactName = contactName
28
+ @contactEmail = contactEmail
29
+ @creator = creator
30
+ @creatorName = creatorName
31
+ @title = title
32
+ @description = description
33
+ @issued = issued
34
+ @modified = modified
35
+ @hasVersion = hasVersion
36
+ @publisher = publisher
37
+ @identifier = identifier
38
+ @license = license
39
+ @language = language
40
+
41
+ @dataset = dataset
42
+ @keyword = keyword
43
+ @landingPage = landingPage
44
+ @qualifiedRelation = qualifiedRelation
45
+ @theme = theme
46
+ @service = service # this is now defunct, I think
47
+ @themeTaxonomy = themeTaxonomy
48
+ @homepage = homepage
49
+
50
+ @serverURL = RDF::URI(serverURL)
51
+ @baseURI = RDF::URI(baseURI)
52
+ @parentURI = RDF::URI(parentURI)
53
+ @types = types
54
+
55
+ abort "you must set baseURI and serverURL parameters" unless self.baseURI and self.serverURL
56
+
57
+ set_headers
58
+ end
59
+
60
+ def set_headers
61
+ return if $headers
62
+
63
+ puts ENV.fetch("FDPUSER", nil)
64
+ puts ENV.fetch("FDPPASS", nil)
65
+ payload = '{ "email": "' + ENV.fetch("FDPUSER", nil) + '", "password": "' + ENV.fetch("FDPPASS", nil) + '" }'
66
+ warn "#{serverURL}/tokens", payload
67
+ resp = RestClient.post("#{serverURL}/tokens", payload, headers = { content_type: "application/json" })
68
+ $token = JSON.parse(resp.body)["token"]
69
+ puts $token
70
+ $headers = { content_type: "text/turtle", authorization: "Bearer #{$token}", accept: "text/turtle" }
71
+ end
72
+
73
+ def build
74
+ @g = RDF::Graph.new # reset graph
75
+ abort "an identifier has not been set" unless identifier
76
+ types.each do |type|
77
+ g << [identifier, RDF.type, type]
78
+ end
79
+
80
+ g << [identifier, RDF::Vocab::RDFS.label, @title] if @title
81
+ g << [identifier, RDF::Vocab::DC.isPartOf, @parentURI] if @parentURI
82
+
83
+ # DCAT
84
+ %w[landingPage qualifiedRelation themeTaxonomy endpointURL endpointDescription].each do |f|
85
+ (pred, value) = get_pred_value(f, "DCAT")
86
+ next unless pred and value
87
+
88
+ g << [identifier, pred, value]
89
+ end
90
+ # DCAT Multi-value
91
+ %w[keyword].each do |f|
92
+ (pred, value) = get_pred_value(f, "DCAT") # value is a comma-separated string
93
+ next unless pred and value
94
+
95
+ keywords = value.split(",")
96
+ keywords.each do |kw|
97
+ kw.strip!
98
+ next if kw.empty?
99
+
100
+ g << [identifier, pred, kw]
101
+ end
102
+ end
103
+
104
+ # DCT
105
+ %w[accessRights hasVersion conformsTo title description identifier license language creator].each do |f|
106
+ (pred, value) = get_pred_value(f, "DCT")
107
+ next unless pred and value
108
+
109
+ g << [identifier, pred, value]
110
+ end
111
+ %w[issued modified].each do |f|
112
+ warn "doing issued modified #{f}"
113
+ (pred, value) = get_pred_value(f, "DCT", "TIME")
114
+ next unless pred and value
115
+
116
+ g << [identifier, pred, value]
117
+ g << [identifier, BS.issued, value]
118
+ g << [identifier, BS.modified, value]
119
+ end
120
+
121
+ # FOAF
122
+ %w[homepage].each do |f|
123
+ (pred, value) = get_pred_value(f, "FOAF")
124
+ next unless pred and value
125
+
126
+ g << [identifier, pred, value]
127
+ end
128
+
129
+ # COMPLEX
130
+
131
+ # identifier
132
+ # contactPoint
133
+ if contactEmail or contactName
134
+ bnode = RDF::URI.new(identifier.to_s + "#contact")
135
+ g << [identifier, DCAT.contactPoint, bnode]
136
+ g << [bnode, RDF.type, RDF::URI.new("http://www.w3.org/2006/vcard/ns#Individual")]
137
+ g << [bnode, RDF::URI.new("http://www.w3.org/2006/vcard/ns#fn"), contactName] if contactName
138
+ g << [bnode, RDF::URI.new("http://www.w3.org/2006/vcard/ns#hasEmail"), contactEmail] if contactEmail
139
+ end
140
+
141
+ # publisher
142
+ if publisher
143
+ bnode = RDF::Node.new
144
+ g << [identifier, RDF::Vocab::DC.publisher, bnode]
145
+ g << [bnode, RDF.type, FOAF.Agent]
146
+ g << [bnode, FOAF.name, publisher]
147
+ end
148
+
149
+ # creator
150
+ if creator
151
+ g << [identifier, RDF::Vocab::DC.creator, RDF::URI.new(creator)]
152
+ g << [RDF::URI.new(creator), RDF.type, FOAF.Agent]
153
+ g << [RDF::URI.new(creator), FOAF.name, creatorName] if creatorName
154
+ end
155
+
156
+ # accessRights
157
+ if accessRights
158
+ g << [identifier, RDF::Vocab::DC.accessRights, RDF::URI.new(accessRights)]
159
+ g << [RDF::URI.new(accessRights), RDF.type, RDF::Vocab::DC.RightsStatement]
160
+ end
161
+
162
+ # dataService
163
+ if is_a? DCATDataService
164
+ warn inspect
165
+ warn "serializing data service #{endpointDescription} or #{endpointURL}"
166
+ if endpointDescription or endpointURL
167
+ warn "serializing ENDPOINTS"
168
+ bnode = RDF::Node.new
169
+ g << [identifier, DCAT.accessService, bnode]
170
+ g << [bnode, RDF.type, DCAT.dataService]
171
+ if endpointDescription
172
+ g << [bnode, DCAT.endpointDescription,
173
+ RDF::URI.new(endpointDescription)]
174
+ end
175
+ g << [bnode, DCAT.endpointURL, RDF::URI.new(endpointURL)] if endpointURL
176
+ end
177
+ end
178
+
179
+ # mediaType or format https://www.iana.org/assignments/media-types/application/3gppHalForms+json
180
+ if is_a? DCATDistribution
181
+ if mediaType
182
+ # CHANGE THIS BACK WHEN FDP SHACL validation is correct
183
+ # type = "https://www.iana.org/assignments/media-types/" + self.mediaType
184
+ # type = RDF::URI.new(type)
185
+ type = mediaType
186
+ g << [identifier, DCAT.mediaType, type]
187
+ # CHANGE THIS BACK ALSO!
188
+ # self.g << [type, RDF.type, RDF::Vocab::DC.MediaType]
189
+ end
190
+ if self.format
191
+ type = RDF::URI.new(self.format)
192
+ g << [identifier, RDF::Vocab::DC.format, type]
193
+ g << [type, RDF.type, RDF::Vocab::DC.MediaTypeOrExtent]
194
+ end
195
+ # conformsTo
196
+ if conformsTo
197
+ schema = RDF::URI.new(conformsTo)
198
+ g << [identifier, RDF::Vocab::DC.conformsTo, schema]
199
+ g << [schema, RDF.type, RDF::Vocab::DC.Standard]
200
+ end
201
+
202
+ end
203
+
204
+ # catalog dataset distribution
205
+ if is_a? DCATCatalog and !datasets.empty?
206
+ datasets.each do |d|
207
+ g << [identifier, DCAT.dataset, RDF::URI.new(d.identifier)]
208
+ end
209
+ elsif is_a? DCATCatalog and !accessServices.empty?
210
+ accessServices.each do |d|
211
+ g << [identifier, DCAT.service, RDF::URI.new(d.identifier)]
212
+ end
213
+ elsif is_a? DCATDataset and !distributions.empty?
214
+ distributions.each do |d|
215
+ g << [identifier, DCAT.distribution, RDF::URI.new(d.identifier)]
216
+ end
217
+ elsif is_a? DCATDistribution and !accessServices.empty?
218
+ accessServices.each do |d|
219
+ g << [identifier, DCAT.accessService, RDF::URI.new(d.identifier)]
220
+ end
221
+ end
222
+
223
+ # theme
224
+ return unless theme
225
+
226
+ themes = theme.split(",").filter_map { |url| url.strip unless url.strip.empty? }
227
+ themes.each do |theme|
228
+ g << [identifier, DCAT.theme, RDF::URI.new(theme)]
229
+ g << [RDF::URI.new(theme), RDF.type, RDF::Vocab::SKOS.Concept]
230
+ g << [RDF::URI.new(theme), RDF::Vocab::SKOS.inScheme,
231
+ RDF::URI.new(identifier.to_s + "#conceptscheme")]
232
+ end
233
+ g << [RDF::URI.new(identifier.to_s + "#conceptscheme"), RDF.type,
234
+ RDF::Vocab::SKOS.ConceptScheme]
235
+ end
236
+
237
+ def serialize(format: :turtle)
238
+ @g.dump(:turtle)
239
+ end
240
+
241
+ def publish
242
+ location = identifier.to_s.gsub(baseURI, serverURL)
243
+ begin
244
+ resp = RestClient.put("#{location}/meta/state", '{ "current": "PUBLISHED" }',
245
+ headers = { authorization: "Bearer #{$token}", content_type: "application/json" })
246
+ warn "publish response message"
247
+ warn resp.inspect
248
+ rescue StandardError
249
+ warn "ERROR in publishing"
250
+ end
251
+ end
252
+
253
+ def get_pred_value(pred, vocab, datatype = nil)
254
+ # $stderr.puts "getting #{pred}, #{vocab}"
255
+ urire = Regexp.new("((http|https)://)(www.)?[a-zA-Z0-9@:%._\\+~#?&//=]{2,256}\\.[a-z]{2,8}\\b([-a-zA-Z0-9@:%._\\+~#?&//=]*)")
256
+ sym = "@" + pred
257
+ # $stderr.puts "getting #{pred}, #{sym}..."
258
+ case vocab
259
+ when "DCT"
260
+ pred = RDF::Vocab::DC[pred]
261
+ when "DCAT"
262
+ pred = DCAT[pred]
263
+ when "FOAF"
264
+ pred = FOAF[pred]
265
+ end
266
+ # $stderr.puts "got #{pred}, #{vocab}"
267
+
268
+ value = instance_variable_get(sym).to_s
269
+ thisvalue = value # temp compy
270
+ # $stderr.puts "got2 #{pred}, #{value}"
271
+
272
+ if datatype == "TIME"
273
+ now = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%L")
274
+ value = RDF::Literal.new(thisvalue, datatype: RDF::URI("http://www.w3.org/2001/XMLSchema#dateTime"))
275
+ warn "time value1 #{value}"
276
+ unless value.valid?
277
+ thisvalue += "T12:00+01:00" # make a guess that they only provided the date
278
+ value = RDF::Literal.new(thisvalue, datatype: RDF::URI("http://www.w3.org/2001/XMLSchema#dateTime"))
279
+ warn "time value2 #{value}"
280
+ unless value.valid?
281
+ value = RDF::Literal.new(now, datatype: RDF::URI("http://www.w3.org/2001/XMLSchema#dateTime"))
282
+ warn "time value3 #{value}"
283
+ end
284
+ end
285
+ elsif urire.match(thisvalue)
286
+ value = RDF::URI.new(thisvalue)
287
+ end
288
+ return [nil, nil] if value.to_s.empty?
289
+
290
+ warn "returning #{pred}, #{value}"
291
+ [pred, value]
292
+ end
293
+ end
294
+
295
+ # %w() array of strings
296
+ # %r() regular expression.
297
+ # %q() string
298
+ # %x() a shell command (returning the output string)
299
+ # %i() array of symbols (Ruby >= 2.0.0)
300
+ # %s() symbol
301
+ # %() (without letter) shortcut for %Q()
302
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FDP
4
+ class Client
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rest-client"
4
+ require "json"
5
+
6
+ module FDP
7
+ ##
8
+ # Client for interacting with a FAIR Data Point (FDP) reference implementation server.
9
+ #
10
+ # This class handles authentication (token-based) and provides methods for
11
+ # discovering and managing metadata schemas (SHACL shapes) and resource definitions.
12
+ #
13
+ # == Authentication
14
+ #
15
+ # The client obtains a JWT bearer token via the +/tokens+ endpoint on initialization.
16
+ # All subsequent requests use this token in the +Authorization+ header.
17
+ #
18
+ # == Usage example
19
+ #
20
+ # client = FDP::Client.new(
21
+ # base_url: "https://example.com/fdp",
22
+ # email: "user@example.com",
23
+ # password: "secret123"
24
+ # )
25
+ #
26
+ # schemas = client.retrieve_current_schemas
27
+ # resources = client.retrieve_current_resources
28
+ #
29
+ class Client
30
+ # @return [String] Base URL of the FAIR Data Point server (without trailing slash preferred)
31
+ attr_accessor :base_url
32
+
33
+ # @return [String] Email used for authentication
34
+ attr_accessor :email
35
+
36
+ # @return [String] Password used for authentication (stored only temporarily)
37
+ attr_accessor :password
38
+
39
+ # @return [String] JWT bearer token obtained after successful login
40
+ attr_accessor :token
41
+
42
+ # @return [Hash] Default headers used in all authenticated API requests
43
+ attr_accessor :headers
44
+
45
+ ##
46
+ # Initializes a new FDP client and authenticates against the server.
47
+ #
48
+ # @param base_url [String] URL of the FAIR Data Point server (e.g. https://fdp.example.com)
49
+ # @param email [String] User email for authentication (default: albert.einstein@example.com)
50
+ # @param password [String] User password for authentication (default: password)
51
+ #
52
+ # @raise [SystemExit] if authentication fails or unexpected error occurs
53
+ #
54
+ def initialize(base_url:, email: "albert.einstein@example.com", password: "password")
55
+ @base_url = base_url
56
+ @email = email
57
+ @password = password
58
+
59
+ begin
60
+ response = RestClient.post(
61
+ "#{base_url}/tokens",
62
+ { email: email, password: password }.to_json,
63
+ content_type: :json, accept: :json
64
+ )
65
+
66
+ token_data = JSON.parse(response.body)
67
+ @token = token_data["token"]
68
+
69
+ warn "Authorization: Bearer #{@token}"
70
+ rescue RestClient::ExceptionWithResponse => e
71
+ warn "Error getting token:"
72
+ warn "Status: #{e.response.code}"
73
+ warn "Body: #{e.response.body}"
74
+ abort
75
+ rescue StandardError => e
76
+ warn "Unexpected error: #{e.message}"
77
+ abort
78
+ end
79
+
80
+ @headers = {
81
+ Authorization: "Bearer #{@token}",
82
+ accept: :json,
83
+ content_type: :json
84
+ }
85
+ end
86
+
87
+ ##
88
+ # Fetches a simple name → {uuid, definition} lookup of currently defined metadata schemas.
89
+ #
90
+ # @return [Hash<String, Hash>] schema name → { 'uuid' => ..., 'definition' => ... }
91
+ # @return [Hash] empty hash when request fails
92
+ #
93
+ def list_current_schemas
94
+ begin
95
+ response = RestClient.get("#{base_url}/metadata-schemas", headers)
96
+ rescue RestClient::ExceptionWithResponse => e
97
+ warn "Error fetching schemas:"
98
+ warn "Status: #{e.response.code}"
99
+ warn "Body: #{e.response.body}"
100
+ return {}
101
+ rescue StandardError => e
102
+ warn "Unexpected error: #{e.message}"
103
+ return {}
104
+ end
105
+
106
+ j = JSON.parse(response.body)
107
+ uuids = {}
108
+
109
+ j.each do |entry|
110
+ uuids[entry["name"]] = {
111
+ "uuid" => entry["uuid"],
112
+ "definition" => entry["latest"]["definition"]
113
+ }
114
+ end
115
+
116
+ uuids
117
+ end
118
+
119
+ ##
120
+ # Retrieves all currently defined metadata schemas as rich {FDP::Schema} objects.
121
+ #
122
+ # This method is usually preferred over #list_current_schemas when you need
123
+ # full metadata shape information (parents, prefix, target classes, etc.).
124
+ #
125
+ # @return [Array<FDP::Schema>] array of schema objects
126
+ # @return [Array] empty array when request fails
127
+ #
128
+ def retrieve_current_schemas
129
+ begin
130
+ response = RestClient.get("#{base_url}/metadata-schemas", headers)
131
+ rescue RestClient::ExceptionWithResponse => e
132
+ warn "Error fetching schemas:"
133
+ warn "Status: #{e.response.code}"
134
+ warn "Body: #{e.response.body}"
135
+ return []
136
+ rescue StandardError => e
137
+ warn "Unexpected error: #{e.message}"
138
+ return []
139
+ end
140
+
141
+ j = JSON.parse(response.body)
142
+ schemas = []
143
+
144
+ j.each do |entry|
145
+ latest = entry["latest"] || {}
146
+
147
+ schemas << FDP::Schema.new(
148
+ client: self,
149
+ uuid: entry["uuid"],
150
+ name: entry["name"],
151
+ label: latest["suggestedResourceName"],
152
+ description: latest["description"],
153
+ definition: latest["definition"],
154
+ prefix: latest["suggestedUrlPrefix"],
155
+ parents: latest["extendsSchemaUuids"] || [],
156
+ children: latest["childSchemaUuids"] || [],
157
+ version: latest["version"] || "1.0.0",
158
+ targetclasses: latest["targetClassUris"] || ["http://www.w3.org/ns/dcat#Resource"]
159
+ )
160
+ end
161
+
162
+ schemas
163
+ end
164
+
165
+ ##
166
+ # Fetches a simple name → uuid lookup of currently defined resource definitions.
167
+ #
168
+ # @return [Hash<String, Hash>] resource name → { 'uuid' => ... }
169
+ # @return [Hash] empty hash when request fails
170
+ #
171
+ def list_current_resources
172
+ begin
173
+ response = RestClient.get("#{base_url}/resource-definitions", headers)
174
+ rescue RestClient::ExceptionWithResponse => e
175
+ warn "Error fetching resources definitions:"
176
+ warn "Status: #{e.response.code}"
177
+ warn "Body: #{e.response.body}"
178
+ return {}
179
+ rescue StandardError => e
180
+ warn "Unexpected error: #{e.message}"
181
+ return {}
182
+ end
183
+
184
+ j = JSON.parse(response.body)
185
+ uuids = {}
186
+
187
+ j.each do |entry|
188
+ uuids[entry["name"]] = { "uuid" => entry["uuid"] }
189
+ end
190
+
191
+ uuids
192
+ end
193
+
194
+ ##
195
+ # Retrieves all currently defined resource definitions as rich {FDP::Resource} objects.
196
+ #
197
+ # @return [Array<FDP::Resource>] array of resource definition objects
198
+ # @return [Array] empty array when request fails
199
+ #
200
+ def retrieve_current_resources
201
+ begin
202
+ response = RestClient.get("#{base_url}/resource-definitions", headers)
203
+ rescue RestClient::ExceptionWithResponse => e
204
+ warn "Error fetching resources definitions:"
205
+ warn "Status: #{e.response.code}"
206
+ warn "Body: #{e.response.body}"
207
+ return []
208
+ rescue StandardError => e
209
+ warn "Unexpected error: #{e.message}"
210
+ return []
211
+ end
212
+
213
+ j = JSON.parse(response.body)
214
+ resources = []
215
+
216
+ j.each do |entry|
217
+ resources << FDP::Resource.new(resourcejson: entry, client: self)
218
+ end
219
+
220
+ resources
221
+ end
222
+ end
223
+ end