version-one 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.
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ .idea/
24
+ spec/v1config.yml
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.2-p484
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in version-one.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ gem 'rspec-mocks'
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Dan Drew
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Version::One::Gem
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'version-one-gem'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install version-one-gem
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/version-one-gem/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,434 @@
1
+ require 'xml/libxml'
2
+ require 'version-one/asset_ref'
3
+
4
+ module VersionOne
5
+ class Asset
6
+
7
+ ASSET_TYPE_MAP = {
8
+ date: Time,
9
+ text: String,
10
+ numeric: Float,
11
+ rank: Integer
12
+ }.freeze
13
+
14
+ attr_reader :type,
15
+ :href,
16
+ :meta,
17
+ :attributes
18
+
19
+ def initialize(options={})
20
+ raise ArgumentError unless options.is_a?(Hash)
21
+ xml = options.delete(:xml)
22
+ @type = options.delete(:type)
23
+ @initialized = false
24
+ @href = nil
25
+
26
+ if xml
27
+ xml = xml.root if xml.is_a?(XML::Document)
28
+ @id = normalized_id(xml.attributes['id'])
29
+ @href = normalized_href(xml.attributes['href'])
30
+ unless @type
31
+ @type = xml.find_first('Attribute[@name="AssetType"]')
32
+ @type = @type.content
33
+ end
34
+ else
35
+ @id = normalized_id(options[:id]) if options[:id]
36
+ end
37
+
38
+ class_name = self.class.name.split('::').last
39
+ if class_name != 'Asset'
40
+ @type ||= class_name
41
+ raise ArgumentError, "Incompatible class #{class_name} for asset type #{@type}" unless @type == class_name
42
+ end
43
+
44
+ case @type
45
+ when String, Symbol
46
+ @type = @type.to_s
47
+ when NilClass
48
+ raise ArgumentError, 'No asset type specified' unless @type
49
+ else
50
+ raise ArgumentError, "Invalid asset type argument: #{@type.inspect}"
51
+ end
52
+
53
+ @meta = options[:meta]
54
+ @meta ||= options[:meta_cache][@type] if options.key?(:meta_cache)
55
+ @meta ||= Meta.get(@type)
56
+
57
+ @attributes = {}
58
+ @changed_attributes = {}
59
+ @setters = {}
60
+
61
+ @meta.attributes.each do |a|
62
+ simple_type = ASSET_TYPE_MAP[a.type]
63
+ if simple_type
64
+ add_simple_attribute(a, simple_type)
65
+ elsif a.type == :relation
66
+ add_relation_attribute(a)
67
+ else
68
+ add_simple_attribute(a, String)
69
+ end
70
+ end
71
+
72
+ if xml
73
+ xml.each {|el| init_attribute_value(el) }
74
+ @changed_attributes.clear
75
+ else
76
+ options.each do |name,val|
77
+ send("#{name}=", val)
78
+ end
79
+ end
80
+
81
+ @initialized = true
82
+
83
+ self
84
+ end
85
+
86
+ def id
87
+ @id
88
+ end
89
+
90
+ def ref
91
+ AssertRef.for(self)
92
+ end
93
+
94
+ def inspect
95
+ "<Asset:#{@id || @type || '???'}>"
96
+ end
97
+
98
+ def save(options={})
99
+ save!(options) rescue false
100
+ end
101
+
102
+ def save!(options={})
103
+ xml = change_xml
104
+ client = options[:client] || VersionOne::Client.new
105
+ response_xml = client.post_xml(save_url, xml)
106
+ @href ||= normalized_href(response_xml.attributes['href'])
107
+ @id ||= normalized_id(response_xml.attributes['id'])
108
+ @changed_attributes.clear
109
+ self
110
+ end
111
+
112
+ def delete!(options={})
113
+ exec('Delete', options) unless new_record?
114
+ end
115
+
116
+ def delete(options={})
117
+ delete! rescue false
118
+ end
119
+
120
+ def new_record?
121
+ @href.nil?
122
+ end
123
+
124
+ def add_comment(msg)
125
+ raise 'Cannot comment on an unsaved asset' if new_record?
126
+
127
+ conversation = VersionOne::Asset.new(type: 'Conversation')
128
+ conversation.save!
129
+
130
+ expression = VersionOne::Asset.new(type: 'Expression')
131
+ expression.content = msg
132
+ expression.belongs_to = conversation
133
+ expression.mentions = [self]
134
+ expression.save!
135
+ rescue
136
+ conversation.delete if conversation
137
+ raise
138
+ end
139
+
140
+ def exec(op_name, options={})
141
+ client = options[:client] || VersionOne::Client.new
142
+ client.post(href + "?op=#{op_name}")
143
+ end
144
+
145
+ def get_history(fields=nil)
146
+ path = href.sub(/\/[Dd]ata\//, '/Hist/')
147
+ client = VersionOne::Client.new
148
+ Asset.from_xml(client.get(path, fields))
149
+ end
150
+
151
+ def self.from_xml(xml)
152
+ meta = nil
153
+ case xml.name
154
+ when 'Asset'
155
+ Asset.new(xml: xml)
156
+ when 'Assets', 'History'
157
+ xml.find('Asset').map do |el|
158
+ a = Asset.new(xml: el, meta: meta)
159
+ meta ||= a.meta
160
+ a
161
+ end
162
+ else
163
+ raise "XML element #{xml.name} cannot be converted to an asset"
164
+ end
165
+ end
166
+
167
+ def self.get(id_or_href, *fields)
168
+ client = VersionOne::Client.new
169
+ if id_or_href =~ /^[A-Za-z]+:\d+$/
170
+ xml = client.get "/rest.v1/Data/#{id_or_href.sub(':', '/')}", *fields
171
+ else
172
+ xml = client.get id_or_href, *fields
173
+ end
174
+ Asset.from_xml(xml)
175
+ end
176
+
177
+ def reload(*fields)
178
+ Asset.get(self.href, *fields)
179
+ end
180
+
181
+ private
182
+
183
+ def init_attribute_value(el)
184
+ attr_name = el.attributes['name']
185
+
186
+ children = el.children
187
+ child_count = children.size
188
+ first = children[0]
189
+
190
+ if true #child_count > 0
191
+
192
+ case el.name
193
+ when 'Attribute'
194
+ val = case
195
+ when child_count == 0
196
+ nil
197
+ when first.name == 'Value'
198
+ children.map(&:content)
199
+ else
200
+ el.content
201
+ end
202
+
203
+ #DEBUG puts "Setting #{attr_name} to #{val.inspect}"
204
+
205
+ if @setters[attr_name]
206
+ send(@setters[attr_name], val)
207
+ else
208
+ add_simple_attribute(attr_name, String, readonly: true)
209
+ @attributes[attr_name] = val
210
+ end
211
+
212
+ when 'Relation'
213
+ val = children.map{|v| (v.name == 'Asset') ? AssetRef.new(v) : nil}.compact
214
+ unless @setters[attr_name]
215
+ add_relation_attribute(attr_name, readonly: true)
216
+ end
217
+ send(@setters[attr_name], val)
218
+ @attributes[attr_name].unchanged! if @attributes[attr_name].respond_to?(:unchanged!)
219
+
220
+ else
221
+ # Ignore
222
+ end
223
+
224
+ end
225
+ end
226
+
227
+ def add_generic_attribute(attr_name, options={})
228
+ if attr_name.is_a?(VersionOne::AttributeDefinition)
229
+ a = attr_name
230
+ attr_name = a.name
231
+ options = {
232
+ readonly: a.readonly?,
233
+ multivalue: a.multivalue?
234
+ }
235
+ end
236
+
237
+ clean_name = attr_name.gsub('.', '_')
238
+ ruby_name = clean_name.gsub(/([a-z])([A-Z])/){|m|
239
+ $1 + '_' + $2
240
+ }.downcase
241
+ readonly = !!options[:readonly]
242
+
243
+ set_script = %{
244
+ def #{ruby_name}=(val)
245
+ raise "Can't set read-only property" if #{readonly.to_s} && @initialized
246
+ #{yield(attr_name, options)}
247
+ @attributes['#{attr_name}'] = val
248
+ @changed_attributes['#{attr_name}'] = true
249
+ val
250
+ end
251
+ }
252
+
253
+ get_script = %{
254
+ def #{ruby_name}
255
+ @attributes['#{attr_name}']
256
+ end
257
+
258
+ def #{ruby_name}?
259
+ !!#{ruby_name}
260
+ end
261
+
262
+ def #{ruby_name}_definition
263
+ @meta.attributes['#{attr_name}']
264
+ end
265
+ }
266
+
267
+ instance_eval(get_script)
268
+ instance_eval(set_script)
269
+ @setters[attr_name] = "#{ruby_name}="
270
+ end
271
+
272
+ def add_simple_attribute(attr_name, native_type, options={})
273
+
274
+ add_generic_attribute(attr_name, options) do |attr_name, options|
275
+ convert = "convert_to_#{native_type}"
276
+ if options[:multivalue]
277
+ setter = %{
278
+ val = case val
279
+ when NilClass
280
+ val
281
+ when Array
282
+ val.map{|v| #{convert}('#{attr_name}',v) }
283
+ else
284
+ [#{convert}('#{attr_name}',val)]
285
+ end
286
+ }
287
+ else
288
+ setter = %{
289
+ val = case val
290
+ when NilClass
291
+ val
292
+ else
293
+ #{convert}('#{attr_name}',val)
294
+ end
295
+ }
296
+ end
297
+
298
+ setter
299
+ end
300
+
301
+ end
302
+
303
+ def add_relation_attribute(a, opts={})
304
+ add_generic_attribute(a, opts) do |attr_name, options|
305
+ script = %{
306
+ val = case val
307
+ when NilClass
308
+ val
309
+ }
310
+ if options[:multivalue]
311
+ script << %{
312
+ when Asset, AssetRef, Array
313
+ rel = @attributes['#{attr_name}'] || RelationMultiValue.new
314
+ rel.set(val)
315
+ else
316
+ raise ArgumentError, "Invalid argument type: %s" % [val.class.name]
317
+ end
318
+ }
319
+ else
320
+ script << %{
321
+ when Array
322
+ raise ArgumentError.new("Too many values for single value attribute") if val.size > 1
323
+ val[0]
324
+ when Asset, AssetRef
325
+ val
326
+ else
327
+ raise ArgumentError, "Invalid argument type: %s" % [val.class.name]
328
+ end
329
+ val = AssetRef.for(val) if val
330
+ }
331
+ end
332
+ script
333
+ end
334
+ end
335
+
336
+ def convert_to_Integer(name, val)
337
+ val.to_i
338
+ end
339
+
340
+ def convert_to_String(name, val)
341
+ val.to_s
342
+ end
343
+
344
+ def convert_to_Time(name, val)
345
+ VersionOne.s_to_time(val, utc: !!(name.match(/utc$/i)))
346
+ end
347
+
348
+ def save_url
349
+ if @href
350
+ @href
351
+ else
352
+ "rest-1.v1/Data/#{@type}"
353
+ end
354
+ end
355
+
356
+ def change_xml
357
+ asset = XML::Node.new('Asset')
358
+
359
+ @changed_attributes.each_key do |a|
360
+ attr_def = meta[a]
361
+ child = nil
362
+ val = @attributes[a]
363
+
364
+ if attr_def.relation?
365
+ child = XML::Node.new('Relation')
366
+ case
367
+ when val.nil?
368
+ when attr_def.multivalue?
369
+ val.added.each do |v|
370
+ v = v.to_xml
371
+ v.attributes['act'] = 'add'
372
+ child << v
373
+ end
374
+ val.removed.each do |v|
375
+ v = v.to_xml
376
+ v.attributes['act'] = 'remove'
377
+ child << v
378
+ end
379
+ else
380
+ child << val.to_xml
381
+ end
382
+ else
383
+ child = XML::Node.new('Attribute')
384
+ case
385
+ when val.nil?
386
+ #noop
387
+ when attr_def.multivalue?
388
+ val.each do |v|
389
+ child << XML::Node.new('Value', v.to_s)
390
+ end
391
+ else
392
+ child.content = val.to_s
393
+ end
394
+ end
395
+
396
+ child.attributes['name'] = a
397
+ child.attributes['act'] = 'set' unless (attr_def.relation? && attr_def.multivalue?)
398
+ asset << child
399
+ end
400
+
401
+ asset
402
+ end
403
+
404
+ # Remove the moment from the href if any
405
+ def normalized_href(s)
406
+ if s =~ /(\/.+\/[A-Z][a-zA-Z]+\/\d{4,6})(\/\d{4,6})?$/
407
+ $1
408
+ else
409
+ raise ArgumentError, "Invalid href '#{s}'"
410
+ end
411
+ end
412
+
413
+ # Remove the moment from the ID if any
414
+ def normalized_id(s)
415
+ if s =~ /^([A-Z][a-zA-Z]+:\d{4,6})(:\d{4,6})?$/
416
+ $1
417
+ else
418
+ raise ArgumentError, "Invalid ID '#{s}'"
419
+ end
420
+ end
421
+
422
+ end
423
+
424
+ #
425
+ # ASSET TYPE CLASSES
426
+ #
427
+ class Scope < Asset; end
428
+ class Expression < Asset; end
429
+ class Story < Asset; end
430
+ class Defect < Asset; end
431
+ class Conversation < Asset; end
432
+ class Task < Asset; end
433
+ class Test < Asset; end
434
+ end