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.
@@ -0,0 +1,51 @@
1
+ require 'libxml'
2
+
3
+ module VersionOne
4
+ class AssetRef
5
+ attr_reader :href, :id
6
+
7
+ def initialize(xml_or_asset)
8
+ case xml_or_asset
9
+ when Asset
10
+ @asset = xml_or_asset
11
+ @id = @asset.id
12
+ @href = @asset.href
13
+ else
14
+ @href = xml_or_asset.attributes['href']
15
+ @id = xml_or_asset.attributes['idref']
16
+ end
17
+ raise ArgumentError, "Could not get id and href" unless @id && @href
18
+ end
19
+
20
+ def self.for(x)
21
+ x.is_a?(AssetRef) ? x : AssetRef.new(x)
22
+ end
23
+
24
+ def get(*fields)
25
+ @asset ||= get_asset(*fields)
26
+ end
27
+
28
+ def inspect
29
+ "#<AssetRef:#{@href}>"
30
+ end
31
+
32
+ def to_xml
33
+ xml = XML::Node.new('Asset')
34
+ xml.attributes['href'] = @href
35
+ xml.attributes['idref'] = @id
36
+ xml
37
+ end
38
+
39
+ def method_missing(method, *args, &block)
40
+ get.send(method, *args, &block)
41
+ end
42
+
43
+ private
44
+
45
+ def get_asset(*fields)
46
+ client = VersionOne::Client.new
47
+ xml = client.get(href, *fields)
48
+ Asset.new(xml: xml)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,129 @@
1
+ require 'xml/libxml'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'net/https'
5
+
6
+ module VersionOne
7
+ class Client
8
+ XML_CONTENT_TYPE = 'text/xml'
9
+
10
+ def get(path, *fields)
11
+ uri = path_uri(path)
12
+
13
+ unless fields.empty?
14
+ fields.concat(Query::REQUIRED_FIELDS)
15
+ uri.query = "sel=#{fields.join(',')}"
16
+ end
17
+
18
+ get_uri uri
19
+ end
20
+
21
+ def post_xml(path, xml)
22
+ uri = path_uri(path)
23
+ xml = xml.root if xml.respond_to?(:root)
24
+ xml.attributes['href'] = uri.path
25
+ request :post, uri do |r|
26
+ r.body = xml.to_s
27
+ r.content_type = XML_CONTENT_TYPE
28
+ end
29
+ end
30
+
31
+ def post(path)
32
+ uri = path_uri(path)
33
+ request :post, uri do |r|
34
+ r.body = ''
35
+ r.content_type = XML_CONTENT_TYPE
36
+ end
37
+ end
38
+
39
+ def get_uri(uri)
40
+ request :get, uri
41
+ end
42
+
43
+ def cache_store
44
+ #Rails.cache
45
+ end
46
+
47
+ def can_cache?
48
+ @cache && cache_store
49
+ end
50
+
51
+ def self.service_uri
52
+ @service_uri ||= get_service_uri
53
+ end
54
+
55
+ def service_uri
56
+ self.class.service_uri
57
+ end
58
+
59
+ private
60
+
61
+ def self.get_service_uri
62
+ raise "VersionOne service_uri must be configured" unless VersionOne.config.service_uri
63
+ URI.parse(VersionOne.config.service_uri)
64
+ end
65
+
66
+ def path_uri(path)
67
+ if path =~ /^#{service_uri.path}/i
68
+ uri = service_uri.dup
69
+ uri.path = path
70
+ uri
71
+ else
72
+ uri_string = service_uri.to_s + '/' + path.sub(/^\/+/, '')
73
+ URI.parse(uri_string)
74
+ end
75
+ end
76
+
77
+ def request(type, uri)
78
+ xml = nil
79
+ VersionOne.logger.debug('%s %s' % [type.to_s.upcase, uri.to_s])
80
+
81
+ http = Net::HTTP.new(uri.host, uri.port)
82
+ http.use_ssl = uri.scheme == 'https'
83
+
84
+ request_path = uri.path
85
+ request_path += '?' + uri.query if uri.query
86
+
87
+ result = http.start do
88
+ klass = case type
89
+ when :get
90
+ Net::HTTP::Get
91
+ when :post
92
+ Net::HTTP::Post
93
+ else
94
+ raise ArgumentError, "Unhandled request type: #{type}"
95
+ end
96
+ request = klass.new(request_path)
97
+ request.basic_auth(VersionOne.config.user, VersionOne.config.password)
98
+ yield request if block_given?
99
+ response = http.request(request)
100
+
101
+ if response.content_type == XML_CONTENT_TYPE
102
+ #VersionOne.logger.debug response.body
103
+ xml = XML::Document.string(response.body).root
104
+ end
105
+
106
+ if (response.code.to_i / 100) != 2
107
+ handle_error(xml, response, request)
108
+ end
109
+ end
110
+
111
+ xml || true
112
+ end
113
+
114
+ def handle_error(xml, response, request)
115
+ msg = ''
116
+ if xml && (xml.name == 'Error')
117
+ xml.each do |el|
118
+ msg << el.content
119
+ msg << "\n"
120
+ end
121
+ else
122
+ msg = response.body
123
+ end
124
+ VersionOne.logger.error("%s %s\n%s\n%s" % [response.code.to_s, request.path, response.message, msg])
125
+ raise "VersionOne Error: #{response.message} (#{request.path}) #{msg}"
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,40 @@
1
+ require 'base64'
2
+ require 'logger'
3
+
4
+ module VersionOne
5
+ class Configuration
6
+ attr_accessor :user, :password, :service_uri
7
+
8
+ def load(options={})
9
+ raise ArgumentError.new("Parameter must be a hash") unless options.is_a?(Hash)
10
+
11
+ options.each do |k,v|
12
+ send(k.to_s + '=', v)
13
+ end
14
+ end
15
+
16
+ def password_base64=(val)
17
+ self.password=(Base64.decode64(val))
18
+ end
19
+
20
+ end
21
+
22
+ def self.config
23
+ @@config ||= Configuration.new
24
+ end
25
+
26
+ def self.logger=(l)
27
+ @@logger = l
28
+ end
29
+
30
+ def self.logger
31
+ @@logger ||= defined?(Rails) ? Rails.logger : create_logger
32
+ end
33
+
34
+ def self.create_logger
35
+ l = Logger.new(STDOUT)
36
+ l.level = Logger::INFO
37
+ l
38
+ end
39
+
40
+ end
@@ -0,0 +1,50 @@
1
+ module VersionOne
2
+
3
+ class AttributeDefinition
4
+
5
+ def initialize(xml)
6
+ @xml = xml
7
+ @original_type = @xml.attributes['attributetype']
8
+ end
9
+
10
+ def name
11
+ @name ||= @xml.attributes['name']
12
+ end
13
+
14
+ def type
15
+ @type ||= @original_type.downcase.to_sym
16
+ end
17
+
18
+ def multivalue?
19
+ bool('ismultivalue')
20
+ end
21
+
22
+ def readonly?
23
+ bool('isreadonly')
24
+ end
25
+
26
+ def required?
27
+ bool('isrequired')
28
+ end
29
+
30
+ def relation?
31
+ self.type == :relation
32
+ end
33
+
34
+ def attribute?
35
+ !relation?
36
+ end
37
+
38
+ def inspect
39
+ '#<AttributeDefinition:%s:%s>' % [self.name, @original_type]
40
+ end
41
+
42
+ private
43
+
44
+ def bool(name)
45
+ @xml.attributes[name] == 'True'
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,74 @@
1
+ require 'libxml'
2
+ require 'version-one/meta/attribute_definition'
3
+
4
+ module VersionOne
5
+ class Meta
6
+
7
+ attr_reader :name
8
+ attr_reader :attributes
9
+
10
+ def initialize(xml)
11
+ parse_xml(xml)
12
+ end
13
+
14
+ def self.get(asset_type, client=nil)
15
+ xml = get_cached(asset_type)
16
+
17
+ unless xml
18
+ client ||= VersionOne::Client.new
19
+ xml = client.get('/meta.v1/' + asset_type)
20
+ set_cached(asset_type, xml)
21
+ end
22
+
23
+ new(xml)
24
+ end
25
+
26
+ def self.cache
27
+ @@cache ||= nil
28
+ end
29
+
30
+ def self.cache=(c)
31
+ @@cache = c
32
+ end
33
+
34
+ def [](name)
35
+ @named_attributes[name]
36
+ end
37
+
38
+ def inspect
39
+ '#<Meta:%s>' % [self.name]
40
+ end
41
+
42
+ private
43
+
44
+ def parse_xml(xml)
45
+ @name = xml.attributes['name']
46
+ @attributes = []
47
+ @named_attributes = {}
48
+
49
+ xml.each do |child|
50
+ next unless child.element? && (child.name == 'AttributeDefinition')
51
+ a = AttributeDefinition.new(child)
52
+ next if a.name == 'ID'
53
+ @attributes << a
54
+ @named_attributes[a.name] = a
55
+ end
56
+
57
+ end
58
+
59
+ def self.get_cached(asset_type)
60
+ if cache
61
+ xml = XML::Document.string(cache.read("VersionOne/Meta/#{asset_type}")).root rescue nil
62
+ VersionOne.logger.debug("Loaded #{asset_type} meta from cache") if xml
63
+ xml
64
+ else
65
+ nil
66
+ end
67
+ end
68
+
69
+ def self.set_cached(asset_type, meta)
70
+ cache.write("VersionOne/Meta/#{asset_type}", meta.to_s) if cache
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,261 @@
1
+ module VersionOne
2
+
3
+ class Query
4
+ REQUIRED_FIELDS = %w{AssetType}.freeze
5
+
6
+ def initialize(_asset_type, _client=nil)
7
+ @asset_type = _asset_type
8
+ @select = []
9
+ @filter = []
10
+ @order = []
11
+ @cache = nil
12
+ @asof = nil
13
+ @limit = nil
14
+ @offset = nil
15
+ @client = VersionOne::Client.new
16
+ end
17
+
18
+ def dup(&block)
19
+ q = Query.new(@asset_type)
20
+
21
+ [:@select, :@filter, :@order, :@asof, :@limit, :@offset, :@cache, :@client].each do |sym|
22
+ val = instance_variable_get(sym)
23
+ unless val.nil?
24
+ val = val.dup if val.is_a? Array
25
+ q.instance_variable_set(sym, val)
26
+ end
27
+ end
28
+
29
+ q.instance_eval &block
30
+ q
31
+ end
32
+
33
+ def self.primary_work_items
34
+ Query.new 'PrimaryWorkitem'
35
+ end
36
+
37
+ def self.stories
38
+ Query.new 'Story'
39
+ end
40
+
41
+ def self.projects
42
+ Query.new 'Scope'
43
+ end
44
+
45
+ def self.sprints
46
+ Query.new 'Timebox'
47
+ end
48
+
49
+ def self.issues
50
+ Query.new 'Issue'
51
+ end
52
+
53
+ def find_by_url(url)
54
+ xml = nil
55
+ #xml = cache_store.read(@cache[:key]) if can_cache?
56
+ xml ||= @client.get(url)
57
+ #cache_store.write(@cache[:key], xml, @cache[:options]) if can_cache?
58
+
59
+ if xml.name == 'Error'
60
+ msg = 'VersionOne Error: %s (%s)' % [xml.find_first('Message').content, xml.attributes['href']]
61
+ raise msg
62
+ else
63
+ VersionOne::Asset.from_xml(xml)
64
+ end
65
+
66
+ end
67
+
68
+ def find(what)
69
+ url = to_url(what)
70
+ find_by_url(url)
71
+ end
72
+
73
+ def to_url(what=nil)
74
+ what = what.id.to_s if what.is_a?(Asset)
75
+
76
+ what = case what
77
+ when NilClass
78
+ what
79
+ when Integer
80
+ what.to_s
81
+ when /^[A-Z]+((?::\d+)+)$/i
82
+ $1.gsub(':', '/')
83
+ else
84
+ :bad
85
+ end
86
+
87
+ raise ArgumentError, 'Invalid parameter type' if what == :bad
88
+
89
+ url = ['rest-1.v1/Data', @asset_type, what].compact.join('/')
90
+
91
+ query = [
92
+ select_query,
93
+ filter_query,
94
+ page_query,
95
+ order_query,
96
+ asof_query
97
+ ].compact.join('&')
98
+
99
+ if query && !query.empty?
100
+ url << '?'
101
+ url << query
102
+ end
103
+
104
+ url
105
+ end
106
+
107
+ def select(*fields)
108
+ dup do
109
+ @select = @select + fields
110
+ end
111
+ end
112
+
113
+ def where(criteria)
114
+ criteria = case criteria
115
+ when String
116
+ [criteria]
117
+ when Hash
118
+ criteria.map{|k,v| "#{k}='#{v.to_s}'"}
119
+ else
120
+ raise ArgumentError
121
+ end
122
+ dup do
123
+ @filter.concat criteria
124
+ end
125
+ end
126
+
127
+ def for_project_and_children(project_id)
128
+ where("Scope.ParentMeAndUp='Scope:#{project_id}'")
129
+ end
130
+
131
+ def active
132
+ where("IsInactive='false'")
133
+ end
134
+
135
+ def order(attrib, dir=:asc)
136
+ raise ArgumentError unless attrib.is_a? String
137
+ attrib = '-' + attrib if dir == :desc
138
+ dup { @order << attrib }
139
+ end
140
+
141
+ def offset(index)
142
+ dup { @offset = index }
143
+ end
144
+
145
+ def limit(size)
146
+ dup { @limit = size }
147
+ end
148
+
149
+ def asof(date)
150
+ dup do
151
+ @asof = date
152
+ end
153
+ end
154
+
155
+ def first
156
+ limit(1).all.first
157
+ end
158
+
159
+ def all
160
+ find(nil)
161
+ end
162
+
163
+ def cache(key, options={})
164
+ dup do
165
+ @cache = {
166
+ :key => key,
167
+ :options => options
168
+ }
169
+ end
170
+ end
171
+
172
+ def to_a
173
+ all
174
+ end
175
+
176
+ def each(&block)
177
+ to_a.each(&block)
178
+ end
179
+
180
+ private unless ENV['RAILS_ENV'] == 'test'
181
+
182
+ def http_get(uri)
183
+ xml = nil
184
+
185
+ http = Net::HTTP.new(uri.host, uri.port)
186
+ http.use_ssl = true
187
+
188
+ request_path = uri.path
189
+ request_path += '?' + uri.query unless uri.query.blank?
190
+ Rails.logger.debug("Uri path = #{request_path}")
191
+
192
+ http.start do
193
+ request = Net::HTTP::Get.new(request_path)
194
+ request.basic_auth(VersionOne.user, VersionOne.password)
195
+ response = http.request(request)
196
+
197
+ xml = response.body
198
+ Rails.logger.debug(xml)
199
+ end
200
+
201
+ xml
202
+ end
203
+
204
+ def cache_store
205
+ Rails.cache
206
+ end
207
+
208
+ def can_cache?
209
+ @cache && cache_store
210
+ end
211
+
212
+ def select_query
213
+ if @select.empty?
214
+ nil
215
+ else
216
+ REQUIRED_FIELDS.each {|f| @select << f unless @select.include?(f) }
217
+ 'sel=' + uri_escape(@select.join(','))
218
+ end
219
+ end
220
+
221
+ def filter_query
222
+ if @filter.empty?
223
+ nil
224
+ else
225
+ 'where=' + uri_escape(@filter.collect{|s| "(#{s})"}.join(';'))
226
+ end
227
+ end
228
+
229
+ def page_query
230
+ if @limit
231
+ "page=#{@limit},#{@offset || 0}"
232
+ else
233
+ nil
234
+ end
235
+ end
236
+
237
+ def order_query
238
+ if @order.empty?
239
+ nil
240
+ else
241
+ 'sort=' + uri_escape(@order.join(','))
242
+ end
243
+ end
244
+
245
+ def asof_query
246
+ if @asof.nil?
247
+ nil
248
+ else
249
+ 'asof=' + @asof.xmlschema
250
+ end
251
+ end
252
+
253
+ def uri_escape(s)
254
+ @uri_parser ||= URI::Parser.new
255
+ @uri_parser.escape(s, /[^A-za-z0-9\-()']/)
256
+ end
257
+
258
+ end
259
+
260
+ end
261
+