restful-sharepoint 0.1.7 → 0.2.5

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.
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