5pm 0.1.0

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,354 @@
1
+ {
2
+ "activity": {
3
+ "add": {
4
+ "parameters": {
5
+ "sessionId": "xsd:string",
6
+ "activity": "nsc:Activity"
7
+ },
8
+ "response": {
9
+ "activity": "nsc:Activity"
10
+ }
11
+ },
12
+ "update": {
13
+ "parameters": {
14
+ "sessionId": "xsd:string",
15
+ "activity": "nsc:Activity"
16
+ },
17
+ "response": {
18
+ "activity": "nsc:Activity"
19
+ }
20
+ },
21
+ "remove": {
22
+ "parameters": {
23
+ "sessionId": "xsd:string",
24
+ "id": "xsd:integer"
25
+ },
26
+ "response": {
27
+ "status": "xsd:boolean"
28
+ }
29
+ },
30
+ "getList": {
31
+ "parameters": {
32
+ "sessionId": "xsd:string",
33
+ "filter": "nsc:ActivityListFilter",
34
+ "offset": "xsd:integer",
35
+ "count": "xsd:integer",
36
+ "withFiles": "xsd:boolean",
37
+ "withNames": "xsd:boolean"
38
+ },
39
+ "response": {
40
+ "list": "nsc:ActivityList"
41
+ }
42
+ },
43
+ "getFilesList": {
44
+ "parameters": {
45
+ "sessionId": "xsd:string",
46
+ "filter": "nsc:ActivityListFilter",
47
+ "offset": "xsd:integer",
48
+ "count": "xsd:integer"
49
+ },
50
+ "response": {
51
+ "list": "nsc:FileList"
52
+ }
53
+ },
54
+ "getById": {
55
+ "parameters": {
56
+ "sessionId": "xsd:string",
57
+ "id": "xsd:integer",
58
+ "withNames": "xsd:boolean"
59
+ },
60
+ "response": {
61
+ "activity": "nsc:Activity"
62
+ }
63
+ },
64
+ "attachFiles": {
65
+ "parameters": {
66
+ "sessionId": "xsd:string",
67
+ "activityId": "xsd:integer",
68
+ "files": "nsc:FileList"
69
+ },
70
+ "response": {
71
+ "status": "xsd:boolean"
72
+ }
73
+ },
74
+ "removeFile": {
75
+ "parameters": {
76
+ "sessionId": "xsd:string",
77
+ "id": "xsd:integer"
78
+ },
79
+ "response": {
80
+ "status": "xsd:boolean"
81
+ }
82
+ },
83
+ "downloadFile": {
84
+ "parameters": {
85
+ "sessionId": "xsd:string",
86
+ "id": "xsd:integer",
87
+ "stream": "xsd:boolean"
88
+ },
89
+ "response": {
90
+ "file": "nsc:File"
91
+ }
92
+ }
93
+ },
94
+ "authentication": {
95
+ "signIn": {
96
+ "parameters": {
97
+ "login": "xsd:string",
98
+ "password": "xsd:string"
99
+ },
100
+ "response": {
101
+ "auth": "nsc:AuthToken"
102
+ }
103
+ },
104
+ "signOut": {
105
+ "parameters": {
106
+ "sessionId": "xsd:string"
107
+ },
108
+ "response": {
109
+ "signOutState": "xsd:boolean"
110
+ }
111
+ },
112
+ "getAuthorizedUser": {
113
+ "parameters": {
114
+ "sessionId": "xsd:string"
115
+ },
116
+ "response": {
117
+ "user": "nsc:User"
118
+ }
119
+ },
120
+ "nop": {
121
+ "parameters": {
122
+ "sessionId": "xsd:string"
123
+ },
124
+ "response": {
125
+ "nopState": "xsd:boolean"
126
+ }
127
+ },
128
+ "isSsl": {
129
+ "parameters": {},
130
+ "response": {
131
+ "isSsl": "xsd:boolean"
132
+ }
133
+ }
134
+ },
135
+ "projects": {
136
+ "add": {
137
+ "parameters": {
138
+ "sessionId": "xsd:string",
139
+ "project": "nsc:Project"
140
+ },
141
+ "response": {
142
+ "project": "nsc:Project"
143
+ }
144
+ },
145
+ "update": {
146
+ "parameters": {
147
+ "sessionId": "xsd:string",
148
+ "project": "nsc:Project"
149
+ },
150
+ "response": {
151
+ "project": "nsc:Project"
152
+ }
153
+ },
154
+ "remove": {
155
+ "parameters": {
156
+ "sessionId": "xsd:string",
157
+ "id": "xsd:integer"
158
+ },
159
+ "response": {
160
+ "status": "xsd:boolean"
161
+ }
162
+ },
163
+ "getById": {
164
+ "parameters": {
165
+ "sessionId": "xsd:string",
166
+ "id": "xsd:integer"
167
+ },
168
+ "response": {
169
+ "project": "nsc:Project"
170
+ }
171
+ },
172
+ "getAll": {
173
+ "parameters": {
174
+ "sessionId": "xsd:string",
175
+ "loadTeam": "xsd:boolean"
176
+ },
177
+ "response": {
178
+ "list": "nsc:ProjectList"
179
+ }
180
+ },
181
+ "getList": {
182
+ "parameters": {
183
+ "sessionId": "xsd:string",
184
+ "filter": "nsc:ProjectListFilter",
185
+ "offset": "xsd:integer",
186
+ "count": "xsd:integer",
187
+ "loadTeam": "xsd:boolean"
188
+ },
189
+ "response": {
190
+ "list": "nsc:ProjectList"
191
+ }
192
+ }
193
+ },
194
+ "projectsgroups": {
195
+ "add": {
196
+ "parameters": {
197
+ "sessionId": "xsd:string",
198
+ "group": "nsc:ProjectsGroup"
199
+ },
200
+ "response": {
201
+ "group": "nsc:ProjectsGroup"
202
+ }
203
+ },
204
+ "update": {
205
+ "parameters": {
206
+ "sessionId": "xsd:string",
207
+ "group": "nsc:ProjectsGroup"
208
+ },
209
+ "response": {
210
+ "group": "nsc:ProjectsGroup"
211
+ }
212
+ },
213
+ "remove": {
214
+ "parameters": {
215
+ "sessionId": "xsd:string",
216
+ "id": "xsd:integer"
217
+ },
218
+ "response": {
219
+ "status": "xsd:boolean"
220
+ }
221
+ },
222
+ "getById": {
223
+ "parameters": {
224
+ "sessionId": "xsd:string",
225
+ "id": "xsd:integer"
226
+ },
227
+ "response": {
228
+ "group": "nsc:ProjectsGroup"
229
+ }
230
+ },
231
+ "getAll": {
232
+ "parameters": {
233
+ "sessionId": "xsd:string"
234
+ },
235
+ "response": {
236
+ "list": "nsc:ProjectsGroupList"
237
+ }
238
+ }
239
+ },
240
+ "tasks": {
241
+ "add": {
242
+ "parameters": {
243
+ "sessionId": "xsd:string",
244
+ "task": "nsc:Task"
245
+ },
246
+ "response": {
247
+ "task": "nsc:Task"
248
+ }
249
+ },
250
+ "update": {
251
+ "parameters": {
252
+ "sessionId": "xsd:string",
253
+ "task": "nsc:Task"
254
+ },
255
+ "response": {
256
+ "task": "nsc:Task"
257
+ }
258
+ },
259
+ "remove": {
260
+ "parameters": {
261
+ "sessionId": "xsd:string",
262
+ "id": "xsd:integer"
263
+ },
264
+ "response": {
265
+ "status": "xsd:boolean"
266
+ }
267
+ },
268
+ "getById": {
269
+ "parameters": {
270
+ "sessionId": "xsd:string",
271
+ "id": "xsd:integer"
272
+ },
273
+ "response": {
274
+ "task": "nsc:Task"
275
+ }
276
+ },
277
+ "getList": {
278
+ "parameters": {
279
+ "sessionId": "xsd:string",
280
+ "filter": "nsc:TaskListFilter",
281
+ "offset": "xsd:integer",
282
+ "count": "xsd:integer",
283
+ "loadTeam": "xsd:boolean"
284
+ },
285
+ "response": {
286
+ "list": "nsc:TaskList"
287
+ }
288
+ }
289
+ },
290
+ "users": {
291
+ "add": {
292
+ "parameters": {
293
+ "sessionId": "xsd:string",
294
+ "user": "nsc:User"
295
+ },
296
+ "response": {
297
+ "user": "nsc:User"
298
+ }
299
+ },
300
+ "update": {
301
+ "parameters": {
302
+ "sessionId": "xsd:string",
303
+ "user": "nsc:User"
304
+ },
305
+ "response": {
306
+ "user": "nsc:User"
307
+ }
308
+ },
309
+ "remove": {
310
+ "parameters": {
311
+ "sessionId": "xsd:string",
312
+ "id": "xsd:integer"
313
+ },
314
+ "response": {
315
+ "status": "xsd:boolean"
316
+ }
317
+ },
318
+ "getById": {
319
+ "parameters": {
320
+ "sessionId": "xsd:string",
321
+ "id": "xsd:integer"
322
+ },
323
+ "response": {
324
+ "user": "nsc:User"
325
+ }
326
+ },
327
+ "getByEmail": {
328
+ "parameters": {
329
+ "sessionId": "xsd:string",
330
+ "email": "xsd:string"
331
+ },
332
+ "response": {
333
+ "user": "nsc:User"
334
+ }
335
+ },
336
+ "getAll": {
337
+ "parameters": {
338
+ "sessionId": "xsd:string"
339
+ },
340
+ "response": {
341
+ "sessionId": "xsd:string",
342
+ "list": "nsc:UserList"
343
+ }
344
+ },
345
+ "getAuthorizedUser": {
346
+ "parameters": {
347
+ "sessionId": "xsd:string"
348
+ },
349
+ "response": {
350
+ "user": "nsc:User"
351
+ }
352
+ }
353
+ }
354
+ }
@@ -0,0 +1,51 @@
1
+ module FivePM
2
+ module Pagination
3
+ def each_page(filter: nil, offset: 0, count: 100, max_pages: 100, **options)
4
+ unless block_given?
5
+ return enum_for(__method__, filter: filter, offset: offset, count: count, max_pages: max_pages,
6
+ **options)
7
+ end
8
+
9
+ unless OPERATIONS.fetch(@name).key?("getList")
10
+ raise ArgumentError, "this service does not support offset pagination"
11
+ end
12
+ raise ArgumentError, "max_pages must be 1..10000" unless max_pages.is_a?(Integer) && (1..10_000).cover?(max_pages)
13
+
14
+ Request.operation!(@name, "getList", { "offset" => offset, "count" => count })
15
+ previous = nil
16
+ max_pages.times do
17
+ page = list(filter: filter, offset: offset, count: count, **options)
18
+ raw = page.is_a?(Response) ? page.to_h : page
19
+ raw = Envelope.parts(raw, OPERATIONS.fetch(@name).fetch("getList")) if raw.is_a?(Hash)
20
+ items = raw.dig("list", "items") if raw.is_a?(Hash) && raw["list"].is_a?(Hash)
21
+ raise ProtocolError, "pagination expected a list.items array" unless items.is_a?(Array)
22
+
23
+ received = items.length
24
+ signature = JSON.generate(items)
25
+ raise ProtocolError, "pagination received a repeated page" if previous == signature && received.positive?
26
+ raise ProtocolError, "pagination exceeded the requested page size" if received > count
27
+
28
+ yield page
29
+ return self if received.zero? || received < count
30
+
31
+ previous = signature
32
+ offset += received
33
+ end
34
+ raise ProtocolError, "pagination reached max_pages; resume with an explicit offset"
35
+ end
36
+
37
+ def each_item(**options, &block)
38
+ return enum_for(__method__, **options) unless block_given?
39
+
40
+ each_page(**options) do |page|
41
+ items = if page.is_a?(Response)
42
+ page.list.items
43
+ else
44
+ Envelope.parts(page, OPERATIONS.fetch(@name).fetch("getList")).fetch("list").fetch("items")
45
+ end
46
+ items.each(&block)
47
+ end
48
+ self
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,80 @@
1
+ module FivePM
2
+ module Request
3
+ module_function
4
+
5
+ def validate!(parameters, depth = 0)
6
+ raise ArgumentError, "request nesting exceeds 32 levels" if depth > 32
7
+
8
+ case parameters
9
+ when Hash
10
+ parameters.each do |key, value|
11
+ unless (key.is_a?(String) || key.is_a?(Symbol)) && /\A[a-zA-Z_][a-zA-Z0-9_]*\z/.match?(key.to_s)
12
+ raise ArgumentError, "invalid request field name"
13
+ end
14
+
15
+ validate!(value, depth + 1)
16
+ end
17
+ when Array
18
+ parameters.each { |value| validate!(value, depth + 1) }
19
+ when Float
20
+ raise ArgumentError, "request numbers must be finite" unless parameters.finite?
21
+ when String, Integer, TrueClass, FalseClass, NilClass
22
+ # JSON-compatible scalars only; arbitrary objects cannot enter the wire format.
23
+ else
24
+ raise ArgumentError, "unsupported request value type"
25
+ end
26
+ end
27
+
28
+ def operation!(service, operation, parameters)
29
+ validate!(parameters)
30
+ required = case operation
31
+ when "add", "update"
32
+ [Resource::OBJECT_PARAMETERS.fetch(service)]
33
+ when "getById", "remove", "removeFile", "downloadFile"
34
+ ["id"]
35
+ when "getByEmail"
36
+ ["email"]
37
+ when "attachFiles"
38
+ %w[activityId files]
39
+ else
40
+ []
41
+ end
42
+ if required.any? { |key| !parameters.key?(key) || parameters[key].nil? }
43
+ raise ArgumentError, "missing required operation parameters"
44
+ end
45
+
46
+ %w[id activityId].each do |key|
47
+ identifier!(parameters[key]) if parameters.key?(key)
48
+ end
49
+ if parameters.key?("email") && !(parameters["email"].is_a?(String) && !parameters["email"].empty?)
50
+ raise ArgumentError, "email must be a nonempty string"
51
+ end
52
+ if parameters.key?("filter") && !parameters["filter"].nil? && !parameters["filter"].is_a?(Hash)
53
+ raise ArgumentError, "filter must be an object"
54
+ end
55
+
56
+ if %w[add update].include?(operation)
57
+ record = parameters.fetch(required.first)
58
+ raise ArgumentError, "resource attributes must be an object" unless record.is_a?(Hash)
59
+ if operation == "update" && !(record.key?(:id) || record.key?("id"))
60
+ raise ArgumentError, "update requires an id in the attributes"
61
+ end
62
+
63
+ identifier!(record.key?("id") ? record["id"] : record[:id]) if operation == "update"
64
+ end
65
+ %w[offset count].each do |key|
66
+ next unless parameters.key?(key) && !parameters[key].nil?
67
+
68
+ value = parameters[key]
69
+ minimum = key == "count" ? 1 : 0
70
+ raise ArgumentError, "invalid pagination parameter" unless value.is_a?(Integer) && value >= minimum
71
+ end
72
+ end
73
+
74
+ def identifier!(value)
75
+ return if value.is_a?(Integer) || (value.is_a?(String) && /\A[+-]?\d+\z/.match?(value))
76
+
77
+ raise ArgumentError, "identifier must be an integer or decimal integer string"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,41 @@
1
+ module FivePM
2
+ # Convenience wrappers retain the complete upstream JSON envelope. Use #call
3
+ # for less common operations and exact WSDL parameter names.
4
+ class Resource < Service
5
+ include Pagination
6
+
7
+ OBJECT_PARAMETERS = { "projects" => "project", "projectsgroups" => "group", "tasks" => "task",
8
+ "activity" => "activity", "users" => "user" }.freeze
9
+
10
+ def create(attributes)
11
+ attributes = attributes.to_h if attributes.is_a?(Models::Record)
12
+ call("add", **{ OBJECT_PARAMETERS.fetch(@name) => attributes })
13
+ end
14
+
15
+ def update(attributes)
16
+ attributes = attributes.to_h if attributes.is_a?(Models::Record)
17
+ unless attributes.is_a?(Hash) && (attributes.key?(:id) || attributes.key?("id"))
18
+ raise ArgumentError, "update requires an id in the attributes"
19
+ end
20
+
21
+ call("update", **{ OBJECT_PARAMETERS.fetch(@name) => attributes })
22
+ end
23
+
24
+ def get(id)
25
+ call("getById", id: id)
26
+ end
27
+
28
+ def delete(id)
29
+ call("remove", id: id)
30
+ end
31
+
32
+ def list(filter: nil, offset: nil, count: nil, **options)
33
+ operation = OPERATIONS.fetch(@name).key?("getList") ? "getList" : "getAll"
34
+ parameters = options.dup
35
+ { filter: filter, offset: offset, count: count }.each do |key, value|
36
+ parameters[key] = value unless value.nil?
37
+ end
38
+ call(operation, **parameters)
39
+ end
40
+ end
41
+ end