restful-sharepoint 0.1.7 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed833ac3ebadfee98a8b6fa2b9da7cdc28b52123328ebd90ab1ae997850e2461
4
- data.tar.gz: 546418c16f83e2072d0cdd36eade766b99cea7450e1b93533768493f8b8b003c
3
+ metadata.gz: 44cef9ce93eb602accca9fff9cc11e3ee4a5a304b4351c7638b7453d6e789cfc
4
+ data.tar.gz: 950a9322055853209cb87413dd802d3128ec43eac8ab888060bd13940fea1f49
5
5
  SHA512:
6
- metadata.gz: 41d9bd7486df21258be7ef1ffa903607128e303d7f2245d3dc5ec9ed94a44fbc52ee8c5e927380d3fc51fcb70b53f05fe9a393ec504d08acb4d5ebc9d024129f
7
- data.tar.gz: 93134acddffbafb778f7254c955ccc61c2e38818d66d460e065a92ac215d6f44e4759c84f9cd4d33fc9e5289edb2c7cd8c1553ae4144c8867602637cee112c8f
6
+ metadata.gz: 9094e55c0e4d386192d4afa5df01455f93613e54e85edbc71ccf5628110bb7e323a31134fdc4710d509c136b34763c84e30a8ad24b34cc9a97303db7a5ec8684
7
+ data.tar.gz: 5468b96644de62e3338495790bc10ed7963fcd4c66a03886de54b9eaae1533651219f1456ffd746dbfc315f02e42657dcb9a0e87c90dec352529065f2353b65a
data/Gemfile CHANGED
File without changes
data/README.md CHANGED
File without changes
@@ -2,13 +2,18 @@ module RestfulSharePoint
2
2
  class Collection < CommonBase
3
3
  DEFAULT_OPTIONS = {}
4
4
 
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ def_delegators :@collection, :each, :length, :[]
9
+
5
10
  def self.object_class
6
11
  Object
7
12
  end
8
13
 
9
14
  def initialize(parent: nil, connection: nil, collection: nil, options: {})
10
15
  @parent = parent
11
- @connection = connection || @parent.connection # Iterate collection and coerce each into into a ListItem
16
+ @connection = @parent ? @parent.connection : connection
12
17
  self.collection = collection
13
18
  self.options = options
14
19
  end
@@ -22,24 +27,42 @@ module RestfulSharePoint
22
27
 
23
28
  attr_writer :endpoint
24
29
  def endpoint
25
- @endpoint || (raise NotImplementedError, "Endpoint needs to be set")
30
+ @endpoint || (raise NotImplementedError, "Endpoint could not be determined")
26
31
  end
27
32
 
28
33
  def collection=(collection)
29
34
  @collection = collection
30
35
  @collection&.each_with_index do |v,i|
31
- @collection[i] = objectify(v)
36
+ @collection[i] = connection.objectify(v)
32
37
  end
33
- @properties
38
+ @collection
39
+ end
40
+
41
+ def ==(other)
42
+ other.== collection
43
+ end
44
+
45
+ def eql?(other)
46
+ other.eql? collection
34
47
  end
35
48
 
36
49
  def collection
37
50
  @collection || self.collection = connection.get(endpoint, options: @options)
38
51
  end
39
52
 
40
- def values
41
- collection.dup.each { |k,v| properties[k] = v.values if v.is_a?(Object) || v.is_a?(Collection) }
53
+ def to_a
54
+ collection.map do |v|
55
+ case v
56
+ when Object
57
+ v.to_h
58
+ when Collection
59
+ v.to_a
60
+ else
61
+ v
62
+ end
63
+ end
42
64
  end
65
+ alias to_array to_a
43
66
 
44
67
  def next
45
68
  self.new(@connection, @connection.get(collection['__next']))
@@ -48,13 +71,5 @@ module RestfulSharePoint
48
71
  def to_json(*args, &block)
49
72
  collection.to_json(*args, &block)
50
73
  end
51
-
52
- def method_missing(method, *args, &block)
53
- collection.respond_to?(method) ? collection.send(method, *args, &block) : super
54
- end
55
-
56
- def respond_to_missing?(method, include_all = false)
57
- collection.respond_to?(method, include_all)
58
- end
59
74
  end
60
75
  end
File without changes
File without changes
File without changes
File without changes
@@ -1,18 +1,5 @@
1
1
  module RestfulSharePoint
2
2
  class CommonBase
3
- # Converts the given enumerable tree to a collection or object.
4
- def objectify(tree)
5
- if tree['results'] && !tree['results'].empty?
6
- type = tree.dig('__metadata', 'type') || tree.dig('results', 0, '__metadata', 'type')
7
- raise "Object type not specified" unless type
8
- pattern, klass = COLLECTION_MAP.find { |pattern,| pattern.match(type) }
9
- klass ? RestfulSharePoint.const_get(klass).new(parent: self, collection: tree['results']) : tree['results']
10
- elsif tree['__metadata']
11
- type = tree['__metadata']['type']
12
- raise "Collection type not specified" unless type
13
- pattern, klass = OBJECT_MAP.find { |pattern,| pattern.match(type) }
14
- klass ? RestfulSharePoint.const_get(klass).new(parent: self, properties: tree) : tree
15
- end
16
- end
3
+
17
4
  end
18
5
  end
@@ -13,57 +13,73 @@ module RestfulSharePoint
13
13
 
14
14
  attr_reader :site_url
15
15
 
16
+ def get_as_object(path, options: {})
17
+ objectify(get(path, options: options))
18
+ end
19
+
16
20
  def get(path, options: {})
17
21
  request path, :get, options: options
18
22
  end
19
23
 
20
24
  # Path can be either relative to the site URL, or a complete URL itself.
21
25
  # Takes an optional `options` hash which are any number of valid OData query options (dollar sign prefix is added automatically)
22
- # Also takes an optional block that is provided the HTTPI::Request instance, allowing customisation of the request.
23
26
  def request(path, method, options: {}, body: nil)
24
27
  url = URI.parse(path).is_a?(URI::HTTP) ? path : "#{@site_url}#{path}"
25
28
  options_str = options.map { |k,v| "$#{k}=#{CGI.escape v.to_s}" }.join('&')
26
29
  url += "?#{options_str}"
27
- req = HTTPI::Request.new(url: url, headers: {'accept' => 'application/json; odata=verbose'})
28
- req.auth.ntlm(@username, @password) if @username
30
+
31
+ httpclient = HTTPClient.new
32
+ httpclient.set_auth(nil, @username, @password) if @username
33
+ httpclient.ssl_config.set_default_paths
34
+ headers = {'accept' => 'application/json; odata=verbose'}
29
35
  if body
30
- req.body = body.to_json.gsub('/', '\\/') # SharePoint requires forward slashes be escaped in JSON (WTF!!!)
31
- req.headers['Content-Type'] = 'application/json'
32
- req.headers['X-HTTP-Method'] = 'MERGE' # TODO: Extend logic to support all operations
33
- req.headers['If-Match'] = '*'
34
- end
35
- yield(req) if block_given?
36
- LOG.info "Making HTTP request to: #{req.url.to_s}"
37
- response = HTTPI.request(method, req)
38
- # Log each request to file
39
- if @debug
40
- request_name = req.url.path.gsub(/[^\w.]/, '_')
41
- filepath = ::File.join(@debug, "#{DateTime.now.strftime('%FT%R')}_#{request_name}.log")
42
- FileUtils.mkdir_p @debug
43
- ::File.open(filepath, 'w+') do |file|
44
- file.puts response.code
45
- response.headers.each { |k,v| file.puts "#{k}: #{v}" }
46
- file.puts '----------'
47
- file.puts response.body
48
- end
36
+ body = JSON.dump(body).gsub('/', '\\/') # SharePoint requires forward slashes be escaped in JSON (WTF!!!)
37
+ headers['Content-Type'] = 'application/json'
38
+ headers['X-HTTP-Method'] = 'MERGE' # TODO: Extend logic to support all operations
39
+ headers['If-Match'] = '*'
49
40
  end
41
+
42
+ LOG.info "Making HTTP request to: #{url}"
43
+ response = httpclient.request(method, url, nil, body, headers)
44
+
50
45
  if response.body.empty?
51
46
  if response.code >= 300
52
47
  raise RestError, "Server returned HTTP status #{response.code} with no message body."
53
48
  end
54
49
  else
55
50
  if response.headers['Content-Type'].start_with? "application/json"
56
- data_tree = parse(response.body)
51
+ parse response.body
57
52
  else
58
53
  response.body
59
54
  end
60
55
  end
61
56
  end
62
57
 
58
+ # Converts the given enumerable tree to a collection or object.
59
+ def objectify(tree, parent: nil)
60
+ if tree['results']
61
+ type = tree['__metadata']&.[]('type') || tree['results'][0]&.[]('__metadata')&.[]('type') || ''
62
+ pattern, klass = COLLECTION_MAP.any? { |pattern,| pattern.match(type) }
63
+ klass ||= :Collection
64
+ RestfulSharePoint.const_get(klass).new(connection: self, parent: parent, collection: tree['results'])
65
+ elsif tree['__metadata']
66
+ type = tree['__metadata']['type']
67
+ raise "Object type not specified. #{tree.inspect}" unless type
68
+ pattern, klass = OBJECT_MAP.find { |pattern,| pattern.match(type) }
69
+ klass ? RestfulSharePoint.const_get(klass).new(connection: self, parent: parent, properties: tree) : tree
70
+ else
71
+ tree
72
+ end
73
+ end
74
+
75
+ def objectified?(v)
76
+ CommonBase === v || !v.is_a?(Hash)
77
+ end
78
+
63
79
  protected
64
80
 
65
81
  def parse(str)
66
- data = JSON.parse(str)
82
+ data = JSON.load(str)
67
83
  raise RestError, "(#{data['error']['code']}): #{data['error']['message']['value']}" if data['error']
68
84
  parse_tree(data['d'])
69
85
  end
@@ -71,7 +87,7 @@ module RestfulSharePoint
71
87
  def parse_tree(tree)
72
88
  indices = tree.respond_to?(:keys) ? tree.keys : 0...tree.length
73
89
  indices.each do |i|
74
- if tree[i].respond_to?(:=~) && tree[i] =~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/
90
+ if tree[i].is_a?(String) && tree[i] =~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/
75
91
  tree[i] = DateTime.parse(tree[i]).new_offset(DateTime.now.offset)
76
92
  elsif tree[i].respond_to?(:gsub!)
77
93
  # Convert relative paths to absolute URL's.
File without changes
@@ -2,10 +2,15 @@ module RestfulSharePoint
2
2
  class Object < CommonBase
3
3
  DEFAULT_OPTIONS = {}
4
4
 
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ def_delegators :properties, :length, :keys
9
+
5
10
  def initialize(parent: nil, connection: nil, properties: nil, id: nil, options: {})
6
11
  raise Error, "Either a parent or connection must be provided." unless parent || connection
7
12
  @parent = parent
8
- @connection = connection || @parent.connection
13
+ @connection = @parent ? @parent.connection : connection
9
14
  self.properties = properties
10
15
  @id = id
11
16
  self.options = options
@@ -19,58 +24,61 @@ module RestfulSharePoint
19
24
 
20
25
  attr_writer :endpoint
21
26
  def endpoint
27
+ @endpoint || self['__metadata']['uri'] || (raise NotImplementedError, "Endpoint could not be determined")
28
+ end
22
29
 
30
+ attr_writer :properties
31
+ def properties
32
+ @properties || self.properties = connection.get(endpoint, options: @options)
23
33
  end
24
34
 
25
- def properties=(properties)
26
- @properties = properties
27
- @properties&.each do |k,v|
28
- if v.respond_to?(:keys) && v['__deferred']
29
- define_singleton_method(k) do |options = {}|
30
- if Hash === properties[k] && properties[k]['__deferred']
31
- fetch_deferred(k, options)
32
- else
33
- warn("`options` have been ignored as `#{k}` has already been loaded") unless options.empty?
34
- properties[k]
35
- end
36
- end
37
- elsif v.respond_to?(:keys) && (v['__metadata'] || v['results'])
38
- @properties[k] = objectify(v)
39
- end
35
+ def [](key, options = {})
36
+ if connection.objectified?(properties[key])
37
+ warn "`options` have been ignored as deferred object has already been fetched" unless options.empty?
38
+ properties[key]
39
+ elsif properties[key].respond_to?('[]') && properties[key]['__deferred']
40
+ properties[key] = fetch_deferred(key, options)
41
+ else
42
+ properties[key] = connection.objectify(properties[key])
40
43
  end
41
- @properties
42
44
  end
43
45
 
44
- def properties
45
- @properties || self.properties = connection.get(endpoint, options: @options)
46
+ def ==(other)
47
+ other.== properties
48
+ end
49
+
50
+ def eql?(other)
51
+ other.eql? properties
46
52
  end
47
53
 
48
- def values
49
- properties.dup.each { |k,v| properties[k] = v.values if v.is_a?(Object) || v.is_a?(Collection) }
54
+ def to_h
55
+ hash = {}
56
+ properties.each do |k,v|
57
+ hash[k] = case v
58
+ when Object
59
+ v.to_h
60
+ when Collection
61
+ v.to_a
62
+ else
63
+ v
64
+ end
65
+ end
50
66
  end
67
+ alias to_hash to_h
51
68
 
52
69
  def fetch_deferred(property, options = {})
53
- data = connection.get(@properties[property]['__deferred']['uri'], options: options)
54
- @properties[property] = objectify(data)
70
+ connection.get_as_object(@properties[property]['__deferred']['uri'], options: options)
55
71
  end
56
72
 
57
73
  def to_json(*args, &block)
58
74
  properties.to_json(*args, &block)
59
75
  end
60
76
 
61
- def method_missing(method, *args, &block)
62
- if properties.respond_to?(method)
63
- properties.send(method, *args, &block)
64
- elsif self.methods(false).include?(method) # Works around lazily loaded `properties`
65
- self.send(method, *args, &block)
66
- else
67
- super
77
+ def each(&block)
78
+ properties.each do |k,v|
79
+ yield k, self[k]
68
80
  end
69
81
  end
70
82
 
71
- def respond_to_missing?(method, include_all = false)
72
- properties.respond_to?(method, include_all)
73
- end
74
-
75
83
  end
76
84
  end
@@ -3,7 +3,7 @@ module RestfulSharePoint
3
3
 
4
4
  def endpoint
5
5
  url = URI.parse(connection.site_url)
6
- url.path = URI.encode(@properties['ServerRelativeUrl'])
6
+ url.path = Addressable::URI.encode(@properties['ServerRelativeUrl'])
7
7
  url.to_s
8
8
  end
9
9
  end
@@ -11,7 +11,7 @@ module RestfulSharePoint
11
11
 
12
12
  def url
13
13
  url = URI.parse(connection.site_url)
14
- url.path = URI.encode(self['ServerRelativeUrl'])
14
+ url.path = Addressable::URI.encode(self['ServerRelativeUrl'])
15
15
  url.to_s
16
16
  end
17
17
 
File without changes
@@ -3,7 +3,7 @@ module RestfulSharePoint
3
3
 
4
4
  def self.from_title(title, connection)
5
5
  new(connection: connection).tap do |list|
6
- list.define_singleton_method(:endpoint) { "/_api/web/lists/getbytitle('#{URI.encode title}')" }
6
+ list.define_singleton_method(:endpoint) { "/_api/web/lists/getbytitle('#{Addressable::URI.encode title}')" }
7
7
  end
8
8
  end
9
9
 
File without changes
@@ -1,3 +1,3 @@
1
1
  module RestfulSharePoint
2
- VERSION = '0.1.7'
2
+ VERSION = '0.2.5'
3
3
  end
@@ -1,10 +1,10 @@
1
1
  require 'json'
2
- require 'httpi'
3
- require 'curb'
2
+ require 'httpclient'
3
+ require 'addressable'
4
+ require 'rubyntlm'
4
5
  require 'cgi'
5
6
  require 'logger'
6
-
7
- HTTPI.adapter = :curb
7
+ require 'forwardable'
8
8
 
9
9
  module RestfulSharePoint
10
10
  OBJECT_MAP = {
@@ -9,10 +9,10 @@ Gem::Specification.new 'restful-sharepoint', RestfulSharePoint::VERSION do |s|
9
9
  s.homepage = 'https://github.com/Wardrop/restful-sharepoint'
10
10
  s.license = 'MIT'
11
11
  s.files = Dir.glob(`git ls-files`.split("\n") - %w[.gitignore])
12
- s.has_rdoc = 'yard'
13
12
 
14
13
  s.required_ruby_version = '>= 2.0.0'
15
14
 
16
- s.add_dependency 'httpi', '~> 2.4'
17
- s.add_dependency 'curb', '~> 0.9'
15
+ s.add_dependency 'httpclient', '~> 2.8'
16
+ s.add_dependency 'rubyntlm', '~> 0.6'
17
+ s.add_dependency 'addressable', '~> 2.8'
18
18
  end
@@ -0,0 +1,56 @@
1
+ ENV['RUBY_ENV'] = 'production'
2
+
3
+ require 'yaml'
4
+ require_relative '../lib/restful-sharepoint.rb'
5
+
6
+ CONFIG = YAML.load_file(File.join(__dir__, 'config.yml'))
7
+
8
+ module RestfulSharePoint
9
+ describe RestfulSharePoint do
10
+ let :c do
11
+ Connection.new(CONFIG[:site_url], CONFIG[:username], CONFIG[:password])
12
+ end
13
+
14
+ it "can establish a connection" do
15
+ lists = c.get('/_api/web/lists/')
16
+ expect(lists).to be_a(Hash)
17
+ expect(lists['results']).to be_a(Array)
18
+ end
19
+
20
+ it "can work with a collection" do
21
+ lists = Collection.new(connection: c, collection: c.get('/_api/web/lists/')['results'])
22
+ expect(lists.map { |v| v['Title'] }).to include(CONFIG[:test_list_title])
23
+ end
24
+
25
+ it "can work with an object" do
26
+ list = Object.new(connection: c, properties: c.get("/_api/web/lists/getbytitle('#{Addressable::URI.encode CONFIG[:test_list_title]}')"))
27
+ expect(list['Title']).to eq(CONFIG[:test_list_title])
28
+ end
29
+
30
+ it "can automatically get an object or collection" do
31
+ lists = c.get_as_object('/_api/web/lists/')
32
+ list = c.get_as_object("/_api/web/lists/getbytitle('#{Addressable::URI.encode CONFIG[:test_list_title]}')")
33
+ expect(lists).to be_a(Collection)
34
+ expect(list).to be_a(Object)
35
+ end
36
+
37
+ it "can get a list by title" do
38
+ list = List.from_title(CONFIG[:test_list_title], c)
39
+ expect(list['Title']).to eq(CONFIG[:test_list_title])
40
+ end
41
+
42
+ it "can automatically retrieve deferred objects and c
43
+ ollections" do
44
+ list = List.from_title(CONFIG[:test_list_title], c)
45
+ expect(list['Items'][0]['ContentType']['Name']).to be_a(String)
46
+ end
47
+
48
+ it "can give options when retrieving deferred objects" do
49
+ list = List.from_title(CONFIG[:test_list_title], c)
50
+ expect(list['Items'].to_a[0]['ContentType']['Name']).to be_nil
51
+ expect{list['Items', {expand: 'ContentType'}]}.to output.to_stderr # Should warn about deferred object already been fetched
52
+ list2 = List.from_title(CONFIG[:test_list_title], c)
53
+ expect(list2['Items', {expand: 'ContentType'}].to_a[0]['ContentType']['Name']).to be_a(String)
54
+ end
55
+ end
56
+ end
metadata CHANGED
@@ -1,43 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restful-sharepoint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Wardrop
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-04 00:00:00.000000000 Z
11
+ date: 2021-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: httpi
14
+ name: httpclient
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.4'
19
+ version: '2.8'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.4'
26
+ version: '2.8'
27
27
  - !ruby/object:Gem::Dependency
28
- name: curb
28
+ name: rubyntlm
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.9'
33
+ version: '0.6'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.9'
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: addressable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.8'
41
55
  description: Provides a convenient object model to the OData REST API of SharePoint
42
56
  2013 and newer.
43
57
  email: tomw@msc.qld.gov.au
@@ -65,6 +79,7 @@ files:
65
79
  - lib/restful-sharepoint/objects/web.rb
66
80
  - lib/restful-sharepoint/version.rb
67
81
  - restful-sharepoint.gemspec
82
+ - spec/restfulsharepoint_spec.rb
68
83
  homepage: https://github.com/Wardrop/restful-sharepoint
69
84
  licenses:
70
85
  - MIT
@@ -84,8 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
99
  - !ruby/object:Gem::Version
85
100
  version: '0'
86
101
  requirements: []
87
- rubyforge_project:
88
- rubygems_version: 2.7.3
102
+ rubygems_version: 3.1.2
89
103
  signing_key:
90
104
  specification_version: 4
91
105
  summary: Provides a convenient object model to the OData REST API of SharePoint 2013