Class: JIRA::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/jira/base.rb

Overview

This class provides the basic object <-> REST mapping for all JIRA::Resource subclasses, i.e. the Create, Retrieve, Update, Delete lifecycle methods.

Lifecycle methods

Note that not all lifecycle methods are available for all resources, for example some resources cannot be updated or deleted.

Retrieving all resources

client.Resource.all

Retrieving a single resource

client.Resource.find(id)

Creating a resource

resource = client.Resource.build({'name' => '')
resource.save

Updating a resource

resource = client.Resource.find(id)
resource.save('updated_attribute' => 'new value')

Deleting a resource

resource = client.Resource.find(id)
resource.delete

Nested resources

Some resources are not defined in the top level of the URL namespace within the JIRA API, but are always nested under the context of another resource. For example, a JIRA::Resource::Comment always belongs to a JIRA::Resource::Issue.

These resources must be indexed and built from an instance of the class they are nested under:

issue = client.Issue.find(id)
comments = issue.comments
new_comment = issue.comments.build

Constant Summary collapse

QUERY_PARAMS_FOR_SINGLE_FETCH =
Set.new %i[expand fields]
QUERY_PARAMS_FOR_SEARCH =
Set.new %i[expand fields startAt maxResults]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client, options = {}) ⇒ Base

Returns a new instance of Base.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/jira/base.rb', line 72

def initialize(client, options = {})
  @client   = client
  @attrs    = options[:attrs] || {}
  @expanded = options[:expanded] || false
  @deleted  = false

  # If this class has any belongs_to relationships, a value for
  # each of them must be passed in to the initializer.
  self.class.belongs_to_relationships.each do |relation|
    if options[relation]
      instance_variable_set("@#{relation}", options[relation])
      instance_variable_set("@#{relation}_id", options[relation].key_value)
    elsif options["#{relation}_id".to_sym]
      instance_variable_set("@#{relation}_id", options["#{relation}_id".to_sym])
    else
      raise ArgumentError, "Required option #{relation.inspect} missing" unless options[relation]
    end
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *_args) ⇒ Object

Overrides method_missing to check the attribute hash for resources matching method_name and proxies the call to the superclass if no match is found.



301
302
303
304
305
306
307
# File 'lib/jira/base.rb', line 301

def method_missing(method_name, *_args)
  if attrs.key?(method_name.to_s)
    attrs[method_name.to_s]
  else
    super(method_name)
  end
end

Instance Attribute Details

#attrsObject

The hash of attributes belonging to this instance. An exact representation of the JSON returned from the JIRA API



67
68
69
# File 'lib/jira/base.rb', line 67

def attrs
  @attrs
end

#clientObject (readonly)

A reference to the JIRA::Client used to initialize this resource.



57
58
59
# File 'lib/jira/base.rb', line 57

def client
  @client
end

#deletedObject Also known as: deleted?

Returns true if this instance has been deleted from the server



63
64
65
# File 'lib/jira/base.rb', line 63

def deleted
  @deleted
end

#expandedObject Also known as: expanded?

Returns true if this instance has been fetched from the server



60
61
62
# File 'lib/jira/base.rb', line 60

def expanded
  @expanded
end

Class Method Details

.all(client, options = {}) ⇒ Object

The class methods are never called directly, they are always invoked from a BaseFactory subclass instance.



94
95
96
97
98
99
100
101
# File 'lib/jira/base.rb', line 94

def self.all(client, options = {})
  response = client.get(collection_path(client))
  json = parse_json(response.body)
  json = json[endpoint_name.pluralize] if collection_attributes_are_nested
  json.map do |attrs|
    new(client, { attrs: attrs }.merge(options))
  end
end

.belongs_to(resource) ⇒ Object



263
264
265
266
267
# File 'lib/jira/base.rb', line 263

def self.belongs_to(resource)
  belongs_to_relationships.push(resource)
  attr_reader resource
  attr_reader "#{resource}_id"
end

.belongs_to_relationshipsObject



259
260
261
# File 'lib/jira/base.rb', line 259

def self.belongs_to_relationships
  @belongs_to_relationships ||= []
end

.build(client, attrs) ⇒ Object

Builds a new instance of the resource with the given attributes. These attributes will be posted to the JIRA Api if save is called.



113
114
115
# File 'lib/jira/base.rb', line 113

def self.build(client, attrs)
  new(client, attrs: attrs)
end

.collection_attributes_are_nestedObject



269
270
271
# File 'lib/jira/base.rb', line 269

def self.collection_attributes_are_nested
  @collection_attributes_are_nested ||= false
end

.collection_path(client, prefix = '/') ⇒ Object

Returns the full path for a collection of this resource. E.g.

JIRA::Resource::Issue.collection_path
  # => /jira/rest/api/2/issue


129
130
131
# File 'lib/jira/base.rb', line 129

def self.collection_path(client, prefix = '/')
  client.options[:rest_base_path] + prefix + endpoint_name
end

.endpoint_nameObject

Returns the name of this resource for use in URL components. E.g.

JIRA::Resource::Issue.endpoint_name
  # => issue


121
122
123
# File 'lib/jira/base.rb', line 121

def self.endpoint_name
  name.split('::').last.downcase
end

.find(client, key, options = {}) ⇒ Object

Finds and retrieves a resource with the given ID.



104
105
106
107
108
109
# File 'lib/jira/base.rb', line 104

def self.find(client, key, options = {})
  instance = new(client, options)
  instance.attrs[key_attribute.to_s] = key
  instance.fetch(false, query_params_for_single_fetch(options))
  instance
end

.has_many(collection, options = {}) ⇒ Object

Declares that this class contains a collection of another resource within the JSON returned from the JIRA API.

class Example < JIRA::Base
  has_many :children
end

example = client.Example.find(1)
example.children # Returns an instance of Jira::Resource::HasManyProxy,
                 # which behaves exactly like an array of
                 # Jira::Resource::Child

The following options can be used to override the default behaviour of the relationship:

:attribute_key

The relationship will by default reference a JSON key on the object with the same name as the relationship.

has_many :children # => {"id":"123",{"children":[{"id":"456"},{"id":"789"}]}}

Use this option if the key in the JSON is named differently.

# Respond to resource.children, but return the value of resource.attrs['kids']
has_many :children, :attribute_key => 'kids' # => {"id":"123",{"kids":[{"id":"456"},{"id":"789"}]}}
:class

The class of the child instance will be inferred from the name of the relationship. E.g. has_many :children will return an instance of JIRA::Resource::HasManyProxy containing the collection of JIRA::Resource::Child. Use this option to override the inferred class.

has_many :children, :class => JIRA::Resource::Kid
:nested_under

In some cases, the JSON return from JIRA is nested deeply for particular relationships. This option allows the nesting to be specified.

# Specify a single depth of nesting.
has_many :children, :nested_under => 'foo'
  # => Looks for {"foo":{"children":{}}}
# Specify deeply nested JSON
has_many :children, :nested_under => ['foo', 'bar', 'baz']
  # => Looks for {"foo":{"bar":{"baz":{"children":{}}}}}


245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/jira/base.rb', line 245

def self.has_many(collection, options = {})
  attribute_key = options[:attribute_key] || collection.to_s
  child_class = options[:class] || ('JIRA::Resource::' + collection.to_s.classify).constantize
  self_class_basename = name.split('::').last.downcase.to_sym
  define_method(collection) do
    child_class_options = { self_class_basename => self }
    attribute = maybe_nested_attribute(attribute_key, options[:nested_under]) || []
    collection = attribute.map do |child_attributes|
      child_class.new(client, child_class_options.merge(attrs: child_attributes))
    end
    HasManyProxy.new(self, child_class, collection)
  end
end

.has_one(resource, options = {}) ⇒ Object

Declares that this class contains a singular instance of another resource within the JSON returned from the JIRA API.

class Example < JIRA::Base
  has_one :child
end

example = client.Example.find(1)
example.child # Returns a JIRA::Resource::Child

The following options can be used to override the default behaviour of the relationship:

:attribute_key

The relationship will by default reference a JSON key on the object with the same name as the relationship.

has_one :child # => {"id":"123",{"child":{"id":"456"}}}

Use this option if the key in the JSON is named differently.

# Respond to resource.child, but return the value of resource.attrs['kid']
has_one :child, :attribute_key => 'kid' # => {"id":"123",{"kid":{"id":"456"}}}
:class

The class of the child instance will be inferred from the name of the relationship. E.g. has_one :child will return a JIRA::Resource::Child. Use this option to override the inferred class.

has_one :child, :class => JIRA::Resource::Kid
:nested_under

In some cases, the JSON return from JIRA is nested deeply for particular relationships. This option allows the nesting to be specified.

# Specify a single depth of nesting.
has_one :child, :nested_under => 'foo'
  # => Looks for {"foo":{"child":{}}}
# Specify deeply nested JSON
has_one :child, :nested_under => ['foo', 'bar', 'baz']
  # => Looks for {"foo":{"bar":{"baz":{"child":{}}}}}


194
195
196
197
198
199
200
201
202
# File 'lib/jira/base.rb', line 194

def self.has_one(resource, options = {})
  attribute_key = options[:attribute_key] || resource.to_s
  child_class = options[:class] || ('JIRA::Resource::' + resource.to_s.classify).constantize
  define_method(resource) do
    attribute = maybe_nested_attribute(attribute_key, options[:nested_under])
    return nil unless attribute
    child_class.new(client, attrs: attribute)
  end
end

.key_attributeObject

Returns the attribute name of the attribute used for find. Defaults to :id unless overridden.



149
150
151
# File 'lib/jira/base.rb', line 149

def self.key_attribute
  :id
end

.nested_collections(value) ⇒ Object



273
274
275
# File 'lib/jira/base.rb', line 273

def self.nested_collections(value)
  @collection_attributes_are_nested = value
end

.parse_json(string) ⇒ Object

:nodoc:



153
154
155
# File 'lib/jira/base.rb', line 153

def self.parse_json(string) # :nodoc:
  JSON.parse(string)
end

.singular_path(client, key, prefix = '/') ⇒ Object

Returns the singular path for the resource with the given key. E.g.

JIRA::Resource::Issue.singular_path('123')
  # => /jira/rest/api/2/issue/123

If a prefix parameter is provided it will be injected between the base path and the endpoint. E.g.

JIRA::Resource::Comment.singular_path('456','/issue/123/')
  # => /jira/rest/api/2/issue/123/comment/456


143
144
145
# File 'lib/jira/base.rb', line 143

def self.singular_path(client, key, prefix = '/')
  collection_path(client, prefix) + '/' + key
end

Instance Method Details

#collection_path(prefix = '/') ⇒ Object



315
316
317
318
# File 'lib/jira/base.rb', line 315

def collection_path(prefix = '/')
  # Just proxy this to the class method
  self.class.collection_path(client, prefix)
end

#deleteObject

Sends a delete request to the JIRA Api and sets the deleted instance variable on the object to true.



409
410
411
412
# File 'lib/jira/base.rb', line 409

def delete
  client.delete(url)
  @deleted = true
end

#fetch(reload = false, query_params = {}) ⇒ Object

Fetches the attributes for the specified resource from JIRA unless the resource is already expanded and the optional force reload flag is not set



332
333
334
335
336
337
# File 'lib/jira/base.rb', line 332

def fetch(reload = false, query_params = {})
  return if expanded? && !reload
  response = client.get(url_with_query_params(url, query_params))
  set_attrs_from_response(response)
  @expanded = true
end

#has_errors?Boolean

Returns:

  • (Boolean)


414
415
416
# File 'lib/jira/base.rb', line 414

def has_errors?
  respond_to?('errors')
end

#idObject



277
278
279
# File 'lib/jira/base.rb', line 277

def id
  attrs['id']
end

#key_valueObject

Each resource has a unique key attribute, this method returns the value of that key for this instance.



311
312
313
# File 'lib/jira/base.rb', line 311

def key_value
  @attrs[self.class.key_attribute.to_s]
end

#new_record?Boolean

Determines if the resource is newly created by checking whether its key_value is set. If it is nil, the record is new and the method will return true.

Returns:

  • (Boolean)


461
462
463
# File 'lib/jira/base.rb', line 461

def new_record?
  key_value.nil?
end

#patched_urlObject

This method fixes issue that there is no / prefix in url. It is happened when we call for instance Looks like this issue is actual only in case if you use atlassian sdk your app path is not root (like /jira in example below) issue.save() for existing resource. As a result we got error 400 from JIRA API:

07/Jun/2015:15:32:19 +0400

“PUT jira/rest/api/2/issue/10111 HTTP/1.1” 400 -

After applying this fix we have normal response:

07/Jun/2015:15:17:18 +0400

“PUT /jira/rest/api/2/issue/10111 HTTP/1.1” 204 -



443
444
445
446
447
# File 'lib/jira/base.rb', line 443

def patched_url
  result = url
  return result if result.start_with?('/', 'http')
  "/#{result}"
end

#path_componentObject

This returns the URL path component that is specific to this instance, for example for Issue id 123 it returns ‘/issue/123’. For an unsaved issue it returns ‘/issue’



323
324
325
326
327
# File 'lib/jira/base.rb', line 323

def path_component
  path_component = "/#{self.class.endpoint_name}"
  path_component += '/' + key_value if key_value
  path_component
end

#respond_to?(method_name, _include_all = false) ⇒ Boolean

Checks if method_name is set in the attributes hash and returns true when found, otherwise proxies the call to the superclass.

Returns:

  • (Boolean)


290
291
292
293
294
295
296
# File 'lib/jira/base.rb', line 290

def respond_to?(method_name, _include_all = false)
  if attrs.key?(method_name.to_s)
    true
  else
    super(method_name)
  end
end

#save(attrs, path = url) ⇒ Object

Saves the specified resource attributes by sending either a POST or PUT request to JIRA, depending on resource.new_record?

Accepts an attributes hash of the values to be saved. Will return false if the request fails.



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/jira/base.rb', line 359

def save(attrs, path = url)
  begin
    save_status = save!(attrs, path)
  rescue JIRA::HTTPError => exception
    begin
      set_attrs_from_response(exception.response) # Merge error status generated by JIRA REST API
    rescue JSON::ParserError => parse_exception
      set_attrs('exception' => {
                  'class' => exception.response.class.name,
                  'code' => exception.response.code,
                  'message' => exception.response.message
                })
    end
    # raise exception
    save_status = false
  end
  save_status
end

#save!(attrs, path = nil) ⇒ Object

Saves the specified resource attributes by sending either a POST or PUT request to JIRA, depending on resource.new_record?

Accepts an attributes hash of the values to be saved. Will throw a JIRA::HTTPError if the request fails (response is not HTTP 2xx).



344
345
346
347
348
349
350
351
352
# File 'lib/jira/base.rb', line 344

def save!(attrs, path = nil)
  path ||= new_record? ? url : patched_url
  http_method = new_record? ? :post : :put
  response = client.send(http_method, path, attrs.to_json)
  set_attrs(attrs, false)
  set_attrs_from_response(response)
  @expanded = false
  true
end

#set_attrs(hash, clobber = true, target = nil) ⇒ Object

Set the current attributes from a hash. If clobber is true, any existing hash values will be clobbered by the new hash, otherwise the hash will be deeply merged into attrs. The target paramater is for internal use only and should not be used.



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/jira/base.rb', line 391

def set_attrs(hash, clobber = true, target = nil)
  target ||= @attrs
  if clobber
    target.merge!(hash)
    hash
  else
    hash.each do |k, v|
      if v.is_a?(Hash)
        set_attrs(v, clobber, target[k])
      else
        target[k] = v
      end
    end
  end
end

#set_attrs_from_response(response) ⇒ Object

Sets the attributes hash from a HTTPResponse object from JIRA if it is not nil or is not a json response.



380
381
382
383
384
385
# File 'lib/jira/base.rb', line 380

def set_attrs_from_response(response)
  unless response.body.nil? || (response.body.length < 2)
    json = self.class.parse_json(response.body)
    set_attrs(json)
  end
end

#to_json(options = {}) ⇒ Object

Returns a JSON representation of the current attributes hash.



454
455
456
# File 'lib/jira/base.rb', line 454

def to_json(options = {})
  attrs.to_json(options)
end

#to_sObject



449
450
451
# File 'lib/jira/base.rb', line 449

def to_s
  "#<#{self.class.name}:#{object_id} @attrs=#{@attrs.inspect}>"
end

#to_symObject

Returns a symbol for the given instance, for example JIRA::Resource::Issue returns :issue



283
284
285
# File 'lib/jira/base.rb', line 283

def to_sym
  self.class.endpoint_name.to_sym
end

#urlObject



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/jira/base.rb', line 418

def url
  prefix = '/'
  unless self.class.belongs_to_relationships.empty?
    prefix = self.class.belongs_to_relationships.inject(prefix) do |prefix_so_far, relationship|
      prefix_so_far.to_s + relationship.to_s + '/' + send("#{relationship}_id").to_s + '/'
    end
  end
  if @attrs['self']
    the_url = @attrs['self']
    the_url = the_url.sub(@client.options[:site].chomp('/'), '') if @client.options[:site]
    the_url
  elsif key_value
    self.class.singular_path(client, key_value.to_s, prefix)
  else
    self.class.collection_path(client, prefix)
  end
end