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 +24 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/lib/version-one/asset.rb +434 -0
- data/lib/version-one/asset_ref.rb +51 -0
- data/lib/version-one/client.rb +129 -0
- data/lib/version-one/config.rb +40 -0
- data/lib/version-one/meta/attribute_definition.rb +50 -0
- data/lib/version-one/meta.rb +74 -0
- data/lib/version-one/query.rb +261 -0
- data/lib/version-one/relation_multi_value.rb +50 -0
- data/lib/version-one/time.rb +22 -0
- data/lib/version-one/version.rb +3 -0
- data/lib/version-one/version.rb~ +3 -0
- data/lib/version-one.rb +8 -0
- data/spec/fixtures/story.xml +64 -0
- data/spec/fixtures/story_meta.xml +913 -0
- data/spec/lib/asset_spec.rb +92 -0
- data/spec/lib/common.rb +30 -0
- data/spec/lib/meta_spec.rb +37 -0
- data/spec/lib/query_spec.rb +8 -0
- data/spec/spec_helper.rb +72 -0
- data/spec/v1config.template.yml +11 -0
- data/version-one.gemspec +25 -0
- metadata +130 -0
@@ -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
|
+
|