classlink_client 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6c3a2602b4e9e440690d0b50383257218022c4713a7f2c3e5fa4645fafab35e
4
+ data.tar.gz: 21e291e3ff6ec97970880e0ced7619a5d402e5eb46d68f14794a8c8d336346b2
5
+ SHA512:
6
+ metadata.gz: be2a529532a515dfc506c336769314bc103e4b8df7b66725bfa6eba6f2de1155481666dc8e97a762189c3331d4f72c8e2747f10e231141cb3c94a2554bccf22a
7
+ data.tar.gz: 1b578db8f149324c9e03a295f2a1b9024b03f0703317b50181200508af667f2c44c153f86b182a23ec40b1308121b73fd02d91d681ae20a4fa2ec77054979f45
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,9 @@
1
+ image: ruby:3.0.0
2
+
3
+ before_script:
4
+ - gem install bundler -v 2.2.3
5
+ - bundle install
6
+
7
+ example_job:
8
+ script:
9
+ - bundle exec rake
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ Style/StringLiterals:
2
+ Enabled: true
3
+ EnforcedStyle: double_quotes
4
+
5
+ Style/StringLiteralsInInterpolation:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Layout/LineLength:
10
+ Max: 120
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in classlink_client.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "rubocop", "~> 0.80"
13
+
14
+ gem "pry"
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ classlink_client (0.1.0)
5
+ activesupport (>= 5.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (7.0.3.1)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 1.6, < 2)
13
+ minitest (>= 5.1)
14
+ tzinfo (~> 2.0)
15
+ ast (2.4.2)
16
+ coderay (1.1.3)
17
+ concurrent-ruby (1.1.10)
18
+ i18n (1.12.0)
19
+ concurrent-ruby (~> 1.0)
20
+ method_source (1.0.0)
21
+ minitest (5.16.2)
22
+ parallel (1.22.1)
23
+ parser (3.1.2.1)
24
+ ast (~> 2.4.1)
25
+ pry (0.14.1)
26
+ coderay (~> 1.1)
27
+ method_source (~> 1.0)
28
+ rainbow (3.1.1)
29
+ rake (13.0.6)
30
+ regexp_parser (2.5.0)
31
+ rexml (3.2.5)
32
+ rubocop (0.93.1)
33
+ parallel (~> 1.10)
34
+ parser (>= 2.7.1.5)
35
+ rainbow (>= 2.2.2, < 4.0)
36
+ regexp_parser (>= 1.8)
37
+ rexml
38
+ rubocop-ast (>= 0.6.0)
39
+ ruby-progressbar (~> 1.7)
40
+ unicode-display_width (>= 1.4.0, < 2.0)
41
+ rubocop-ast (1.21.0)
42
+ parser (>= 3.1.1.0)
43
+ ruby-progressbar (1.11.0)
44
+ tzinfo (2.0.5)
45
+ concurrent-ruby (~> 1.0)
46
+ unicode-display_width (1.8.0)
47
+
48
+ PLATFORMS
49
+ x86_64-darwin-20
50
+
51
+ DEPENDENCIES
52
+ classlink_client!
53
+ minitest (~> 5.0)
54
+ pry
55
+ rake (~> 13.0)
56
+ rubocop (~> 0.80)
57
+
58
+ BUNDLED WITH
59
+ 2.2.3
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Raptor Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # ClassLink Ruby Client
2
+
3
+ This is a simple client library for the ClassLink OneRoster API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "classlink_client"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install classlink_client
20
+
21
+ ## Usage
22
+
23
+ ### Authentication
24
+
25
+ Initialize the client using either the Direct, or Proxy Authentication.
26
+
27
+ #### Direct
28
+
29
+ For direct access, pass `endpoint`, `client_id`, and `client_secret`:
30
+
31
+ ```ruby
32
+ client = ClassLink::Client.new(
33
+ endpoint: "https://sandbox-vn-v2.oneroster.com",
34
+ client_id: "some_client_id",
35
+ client_secret: "some_client_secret"
36
+ )
37
+ ```
38
+
39
+ #### Proxy
40
+
41
+ For proxy access, pass `app_id` and `access_token`:
42
+
43
+ ```ruby
44
+ client = ClassLink::Client.new(
45
+ app_id: "some_app_id",
46
+ access_token: "some_access_token"
47
+ )
48
+ ```
49
+
50
+ The resources described in [`lib/classlink_client/interface.rb`](lib/classlink_client/interface.rb) are available as methods on the client. Call them singular with an ID to fetch an individual resource (`client.student(client_id)`), or plural to fetch the index (`client.students`).
51
+
52
+ ### Lazy loading
53
+
54
+ Resources are lazy loaded; `client.students` doesn't actually make any API requests, but `client.students.first` will.
55
+
56
+ ## Development
57
+
58
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
59
+
60
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
61
+
62
+ ## Contributing
63
+
64
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cpoms/classlink_client.
65
+
66
+ ## License
67
+
68
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "classlink_client"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/classlink_client/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "classlink_client"
7
+ spec.version = ClasslinkClient::VERSION
8
+ spec.authors = ["Mike Campbell"]
9
+ spec.email = ["mike.campbell@cpoms.co.uk"]
10
+
11
+ spec.summary = "Client library for ClassLink's OneRoster API"
12
+ spec.homepage = "https://github.com/cpoms/classlink_client"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/cpoms/classlink_client"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "activesupport", ">= 5.0.0"
29
+ end
@@ -0,0 +1,44 @@
1
+ require "uri"
2
+ require_relative "request_signing"
3
+ require_relative "interface"
4
+ require_relative "request_builder"
5
+
6
+ module ClassLink
7
+ class Client
8
+ include Interface
9
+
10
+ DIRECT_OPTIONS = %i(client_id client_secret endpoint)
11
+ PROXY_OPTIONS = %i(access_token app_id)
12
+
13
+ (DIRECT_OPTIONS + PROXY_OPTIONS).each do |opt|
14
+ attr_reader opt
15
+ end
16
+
17
+ attr_reader :base_url, :proxy_api
18
+
19
+ def initialize(options)
20
+ handle_options!(options)
21
+
22
+ # if an endpoint hasn't been supplied, then we're using the proxy
23
+ @proxy_api = !endpoint
24
+ @base_url = endpoint || "https://oneroster-proxy.classlink.io/#{app_id}"
25
+ @base_url << "/ims/oneroster/v1p1/"
26
+ end
27
+
28
+ private
29
+ def handle_options!(options)
30
+ sorted_options = options.keys.sort
31
+
32
+ if sorted_options != DIRECT_OPTIONS && sorted_options != PROXY_OPTIONS
33
+ raise ArgumentError, <<~MSG.squish
34
+ must pass options for direct access (#{DIRECT_OPTIONS}),
35
+ or options for proxy access (#{PROXY_OPTIONS})
36
+ MSG
37
+ end
38
+
39
+ options.each do |opt, value|
40
+ instance_variable_set("@#{opt}", value)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ module ClassLink
2
+ module Interface
3
+ include RequestSigning
4
+
5
+ RESOURCES = %w(
6
+ demographics
7
+ resources
8
+ academic_sessions
9
+ classes
10
+ courses
11
+ enrollments
12
+ orgs
13
+ users
14
+ grading_periods
15
+ schools
16
+ students
17
+ teachers
18
+ terms
19
+ )
20
+
21
+ METHOD_NAME_PROXY = Hash.new{ |h, k| h[k] = k }.merge(
22
+ "classes" => "klasses"
23
+ )
24
+
25
+ RESOURCES.each do |resource|
26
+ define_method METHOD_NAME_PROXY[resource] do |options = {}|
27
+ builder.chain(resource, options)
28
+ end
29
+
30
+ define_method METHOD_NAME_PROXY[resource].singularize do |id|
31
+ builder.chain(resource, { segments: [id] })
32
+ end
33
+ end
34
+
35
+ private
36
+ def builder
37
+ RequestBuilder === self ? self : RequestBuilder.new(self)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,73 @@
1
+ # require "rack"
2
+
3
+ module ClassLink
4
+ class RequestBuilder
5
+ include Interface
6
+ extend Forwardable
7
+
8
+ TEST_ARRAY = [].freeze
9
+ TEST_HASH = {}.freeze
10
+
11
+ def_delegators :@client,
12
+ :base_url,
13
+ :access_token,
14
+ :client_id,
15
+ :client_secret,
16
+ :proxy_api
17
+
18
+ def initialize(client)
19
+ @client = client
20
+
21
+ @path_parts = []
22
+ @query = {}
23
+ @last_request_call = nil
24
+ end
25
+
26
+ def chain(request, options)
27
+ @last_request_call = request
28
+ @query.merge!(options[:query]) if options[:query]
29
+ @path_parts += [request, *options[:segments]].map{ |s| "#{s}/" }
30
+
31
+ self
32
+ end
33
+
34
+ def result
35
+ @result ||= execute
36
+ end
37
+
38
+ def execute
39
+ request(build_url)
40
+ end
41
+
42
+ def build_url
43
+ URI.join(base_url, *@path_parts).tap do |url|
44
+ url.query = @query.to_query
45
+ end
46
+ end
47
+
48
+ def method_missing(method_name, *args, &block)
49
+ # prevent accidental method_missing recursion in development
50
+ # which can make things hard to debug.
51
+ super if @in_method_missing
52
+ @in_method_missing = true
53
+
54
+ if result.respond_to?(method_name)
55
+ result.public_send(method_name, *args, &block)
56
+ else
57
+ super
58
+ end
59
+ ensure
60
+ @in_method_missing = false
61
+ end
62
+
63
+ def respond_to_missing?(method_name, include_private = false)
64
+ if RESOURCES.include?(@last_request_call)
65
+ TEST_ARRAY.respond_to?(method_name)
66
+ elsif RESOURCES.map(&:singularize).include?(@last_request_call)
67
+ TEST_HASH.respond_to?(method_name)
68
+ else
69
+ super
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,88 @@
1
+ require "net/https"
2
+ require "base64"
3
+ require_relative "response"
4
+
5
+ module ClassLink
6
+ module RequestSigning
7
+ def request(url)
8
+ if proxy_api
9
+ get(url, "Bearer #{access_token}")
10
+ else
11
+ get(url, auth_header(url))
12
+ end
13
+ end
14
+
15
+ private
16
+ def auth_header(url)
17
+ # Generate timestamp and nonce
18
+ timestamp = Time.now.to_i.to_s
19
+ nonce = ('A'..'Z').to_a.sample(8).join
20
+
21
+ oauth = {
22
+ "oauth_consumer_key" => client_id,
23
+ "oauth_signature_method" => "HMAC-SHA256",
24
+ "oauth_timestamp" => timestamp,
25
+ "oauth_nonce" => nonce,
26
+ }
27
+
28
+ # Combine oauth params and url params
29
+ params = url.query ? CGI.parse(url.query).transform_values(&:first) : {}
30
+ params.merge!(oauth)
31
+
32
+ # Generate the auth signature
33
+ base_info = build_base_string(url.to_s.split("?")[0], "GET", params)
34
+ composite_key = CGI.escape(client_secret) + "&"
35
+ auth_signature = generate_auth_signature(base_info, composite_key)
36
+ oauth["oauth_signature"] = auth_signature
37
+
38
+ # Generate auth header
39
+ build_auth_header(oauth)
40
+ end
41
+
42
+ def build_base_string(base_url, http_method, params)
43
+ param_string = params.to_query.gsub("+", "%20")
44
+
45
+ http_method + "&" + CGI.escape(base_url) + "&" + CGI.escape(param_string)
46
+ end
47
+
48
+ def generate_auth_signature(base_info, composite_key)
49
+ Base64.encode64(OpenSSL::HMAC::digest(
50
+ OpenSSL::Digest.new("sha256"),
51
+ composite_key,
52
+ base_info
53
+ ))
54
+ end
55
+
56
+ def build_auth_header(oauth)
57
+ values = oauth.map{ |k, v| "#{k}=\"#{CGI.escape(v)}\"" }
58
+ values << values.pop[0..-5] + "\""
59
+ result = "OAuth #{values.join(",")}"
60
+ end
61
+
62
+ def get(url, header)
63
+ all = []
64
+ offset = 0
65
+ limit = 200
66
+ response_size = nil
67
+
68
+ until response_size&.< limit
69
+ url.query = "limit=#{limit}&offset=#{offset}"
70
+ req = Net::HTTP::Get.new(url.request_uri)
71
+ req["Authorization"] = header
72
+ http = Net::HTTP.new(url.hostname, url.port)
73
+ http.use_ssl = true
74
+
75
+ response = Response.new(http.request(req))
76
+
77
+ response_size = response.size
78
+
79
+ if response_size.positive?
80
+ all.concat(response)
81
+ offset += limit
82
+ end
83
+ end
84
+
85
+ all
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ require "json"
2
+
3
+ module ClassLink
4
+ class Response
5
+ extend Forwardable
6
+
7
+ def_delegators :@net_http_response, :code, :message, :uri, :body
8
+
9
+ def initialize(net_http_response)
10
+ @net_http_response = net_http_response
11
+
12
+ raise RequestError unless code.start_with?("2")
13
+ end
14
+
15
+ def parsed
16
+ @parsed ||= JSON.parse(body).then do |res|
17
+ # API responses are keyed (somewhat randomly)
18
+ res.is_a?(Hash) ? res.values[0] : res
19
+ end
20
+ rescue JSON::ParserError
21
+ warn "Error parsing response as JSON, returning raw body instead."
22
+
23
+ body
24
+ end
25
+
26
+ def method_missing(method_name, *args, &block)
27
+ if parsed.respond_to?(method_name)
28
+ parsed.public_send(method_name, *args, &block)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def respond_to_missing?(method_name, include_private = false)
35
+ parsed.respond_to?(method_name) || super
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClasslinkClient
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string"
4
+ require_relative "classlink_client/version"
5
+ require_relative "classlink_client/client"
6
+
7
+ module ClassLink
8
+ class RequestError < StandardError; end
9
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: classlink_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Mike Campbell
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.0
27
+ description:
28
+ email:
29
+ - mike.campbell@cpoms.co.uk
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".gitlab-ci.yml"
36
+ - ".rubocop.yml"
37
+ - Gemfile
38
+ - Gemfile.lock
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - bin/console
43
+ - bin/setup
44
+ - classlink_client.gemspec
45
+ - lib/classlink_client.rb
46
+ - lib/classlink_client/client.rb
47
+ - lib/classlink_client/interface.rb
48
+ - lib/classlink_client/request_builder.rb
49
+ - lib/classlink_client/request_signing.rb
50
+ - lib/classlink_client/response.rb
51
+ - lib/classlink_client/version.rb
52
+ homepage: https://github.com/cpoms/classlink_client
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/cpoms/classlink_client
57
+ source_code_uri: https://github.com/cpoms/classlink_client
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.3.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.3.10
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Client library for ClassLink's OneRoster API
77
+ test_files: []