sls_adf 0.0.1

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.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlsAdf
4
+ module Template
5
+ module Fragment
6
+ User = SlsAdf.client.parse <<~'GRAPHQL'
7
+ fragment Fields on User {
8
+ id
9
+ name
10
+ role
11
+ }
12
+
13
+ fragment StudentFields on User {
14
+ id
15
+ name
16
+ classSerialNo
17
+ level
18
+ }
19
+ GRAPHQL
20
+
21
+ SubjectGroup = SlsAdf.client.parse <<~'GRAPHQL'
22
+ fragment Fields on SubjectGroup {
23
+ uuid
24
+ code
25
+ subject
26
+ }
27
+ GRAPHQL
28
+
29
+ Assignment = SlsAdf.client.parse <<~'GRAPHQL'
30
+ fragment Fields on Assignment {
31
+ uuid
32
+ title
33
+ start
34
+ end
35
+ }
36
+ GRAPHQL
37
+
38
+ Task = SlsAdf.client.parse <<~'GRAPHQL'
39
+ fragment Fields on Task {
40
+ uuid
41
+ title
42
+ start
43
+ end
44
+ subject
45
+ status
46
+ }
47
+ GRAPHQL
48
+
49
+ Notification = SlsAdf.client.parse <<~'GRAPHQL'
50
+ fragment Fields on Notification {
51
+ message
52
+ scope
53
+ scopeId
54
+ eventType
55
+ eventTypeId
56
+ recipient
57
+ }
58
+ GRAPHQL
59
+
60
+ Event = SlsAdf.client.parse <<~'GRAPHQL'
61
+ fragment Fields on Event {
62
+ type
63
+ typeId
64
+ }
65
+ GRAPHQL
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sls_adf/template/fragment'
4
+
5
+ module SlsAdf
6
+ module Template
7
+ module Mutation
8
+ module Assignment
9
+ Create = SlsAdf.client.parse <<~'GRAPHQL'
10
+ mutation($assignment_input: AssignmentInput!) {
11
+ createAssignment(input: $assignment_input) {
12
+ ...SlsAdf::Template::Fragment::Assignment::Fields
13
+ }
14
+ }
15
+ GRAPHQL
16
+
17
+ Update = SlsAdf.client.parse <<~'GRAPHQL'
18
+ mutation($uuid: UUID!, $assignment_input: AssignmentInput!) {
19
+ updateAssignment(uuid: $uuid, input: $assignment_input) {
20
+ ...SlsAdf::Template::Fragment::Assignment::Fields
21
+ }
22
+ }
23
+ GRAPHQL
24
+
25
+ Delete = SlsAdf.client.parse <<~'GRAPHQL'
26
+ mutation($uuid: UUID!) {
27
+ deleteAssignment(uuid: $uuid)
28
+ }
29
+ GRAPHQL
30
+ end
31
+
32
+ module Task
33
+ Update = SlsAdf.client.parse <<~'GRAPHQL'
34
+ mutation($uuid: UUID!, $task_status: TaskStatus!) {
35
+ updateTask(uuid: $uuid, status: $task_status) {
36
+ ...SlsAdf::Template::Fragment::Task::Fields
37
+ }
38
+ }
39
+ GRAPHQL
40
+ end
41
+
42
+ module Notification
43
+ Add = SlsAdf.client.parse <<~'GRAPHQL'
44
+ mutation($notification_input: NotificationInput!) {
45
+ createNotification(input: $notification_input) {
46
+ ...SlsAdf::Template::Fragment::Notification::Fields
47
+ }
48
+ }
49
+ GRAPHQL
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sls_adf/template/fragment'
4
+
5
+ module SlsAdf
6
+ module Template
7
+ module Query
8
+ User = SlsAdf.client.parse <<~'GRAPHQL'
9
+ query($id: ID!, $first: Int = 2) {
10
+ user(id: $id) {
11
+ ...SlsAdf::Template::Fragment::User::Fields
12
+ subjectGroups(first: $first) {
13
+ ...SlsAdf::Template::Fragment::SubjectGroup::Fields
14
+ }
15
+ }
16
+ }
17
+ GRAPHQL
18
+
19
+ SubjectGroup = SlsAdf.client.parse <<~'GRAPHQL'
20
+ query($uuid: UUID!, $first_student: Int = 2, $first_teacher: Int = 2) {
21
+ subjectGroup(uuid: $uuid){
22
+ ...SlsAdf::Template::Fragment::SubjectGroup::Fields
23
+ assignments
24
+ students(first: $first_student, sortField:CLASS_SERIAL_NO) {
25
+ ...SlsAdf::Template::Fragment::User::StudentFields
26
+ }
27
+ teachers(first: $first_teacher) {
28
+ ...SlsAdf::Template::Fragment::User::Fields
29
+ }
30
+ }
31
+ }
32
+ GRAPHQL
33
+
34
+ Assignment = SlsAdf.client.parse <<~'GRAPHQL'
35
+ query($uuid: UUID!, $first: Int = 2) {
36
+ assignment(uuid: $uuid){
37
+ ...SlsAdf::Template::Fragment::Assignment::Fields
38
+ createdBy {
39
+ ...SlsAdf::Template::Fragment::User::Fields
40
+ }
41
+ modifiedBy {
42
+ ...SlsAdf::Template::Fragment::User::Fields
43
+ }
44
+ tasks(first: $first) {
45
+ ...SlsAdf::Template::Fragment::Task::Fields
46
+ }
47
+ }
48
+ }
49
+ GRAPHQL
50
+
51
+ Task = SlsAdf.client.parse <<~'GRAPHQL'
52
+ query($uuid: UUID!) {
53
+ task(uuid: $uuid) {
54
+ ...SlsAdf::Template::Fragment::Task::Fields
55
+ createdBy {
56
+ ...SlsAdf::Template::Fragment::User::Fields
57
+ }
58
+ assignee {
59
+ ...SlsAdf::Template::Fragment::User::StudentFields
60
+ }
61
+ }
62
+ }
63
+ GRAPHQL
64
+
65
+ Context = SlsAdf.client.parse <<~'GRAPHQL'
66
+ query($uuid: UUID!, $first: Int = 15) {
67
+ context(uuid: $uuid) {
68
+ event {
69
+ ...SlsAdf::Template::Fragment::Event::Fields
70
+ }
71
+ user {
72
+ ...SlsAdf::Template::Fragment::User::Fields
73
+ subjectGroups(first: $first) {
74
+ ...SlsAdf::Template::Fragment::SubjectGroup::Fields
75
+ }
76
+ }
77
+ }
78
+ }
79
+ GRAPHQL
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlsAdf
4
+ # Module used to declare Query, Mutation and Fragments to be used.
5
+ module Template; end
6
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typhoeus'
4
+ require 'json'
5
+
6
+ module SlsAdf
7
+ module Util
8
+ # Custom adapter written for github/graphql-client, built to achieve 2 things:
9
+ # 1. Expose HTTP status of response through the #execute method.
10
+ # 2. Automatically GETs token from token endpoint if call is unauthorised.
11
+ #
12
+ # Adapter can be customised with special logic when calling SLS ADF APIs.
13
+ class Adapter
14
+ # GraphQL execution adapter used with the graphql-client library.
15
+ # The Adapter must respond to the execute method with the following method
16
+ # signature.
17
+ #
18
+ # Link: https://github.com/github/graphql-client/blob/master/guides/remote-queries.md
19
+ #
20
+ # @param [GraphQL::Language::Nodes::Document] document The query itself.
21
+ # @param [String] operation_name The name of the operation
22
+ # @param [Hash] variables A hash of the query variables.
23
+ # @param [Hash] context Arbitary hash of values that can be accessed
24
+ # @return [Hash] Parsed API response. Sample shape:
25
+ # If successful: { 'data' => ... }
26
+ # If unsuccessful: { 'errors' => ... }
27
+ def execute(document:, operation_name: nil, variables: {}, context: {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
28
+ body = {}
29
+ # Convert document into query parameters
30
+ body['query'] = document.to_query_string
31
+ body['operationName'] = operation_name if operation_name
32
+ body['variables'] = variables if variables.any?
33
+
34
+ headers = context.merge(authorization_header(token.token))
35
+ response = execute_call(headers: headers, body: body)
36
+
37
+ if response.code == 401 # Unauthorized
38
+ new_headers = context.merge(authorization_header(token.refresh_token))
39
+ response = execute_call(headers: new_headers, body: body)
40
+ end
41
+
42
+ if response.code.zero?
43
+ { errors: [{ message: 'Unable to establish a connection' }] }
44
+ else
45
+ JSON.parse(response.body).merge(http_status: response.code)
46
+ end
47
+ rescue JSON::ParserError
48
+ { errors: [{ message: 'JSON parsing failed' }] }
49
+ end
50
+
51
+ private
52
+
53
+ # Returns a hash for the autorizatiom key and value.
54
+ #
55
+ # @param [String] token Actual token to be inserted.
56
+ # @return [Hash] Authorization header key and values
57
+ def authorization_header(token)
58
+ { Authorization: 'Bearer ' + token }
59
+ end
60
+
61
+ # Executes a HTTP POST call to the specified GraphQL URL.
62
+ #
63
+ # @param [Hash] headers Additional headers to be included, other than
64
+ # those specified in +SlsAdf::Util::COMMON_HEADERS+.
65
+ # @param [Hash] body The body to be sent in the POST call.
66
+ # @return [Typhoeus::Response] The response object.
67
+ def execute_call(headers: {}, body: {})
68
+ headers = COMMON_HEADERS.merge(headers)
69
+ Typhoeus.post(graphql_url, headers: headers, body: JSON.dump(body))
70
+ end
71
+
72
+ # Reference to the Token object. Can be over-written with a custom
73
+ # implementation of the Token object, offering +Token.token+ and
74
+ # +Token.refresh_token+ methods.
75
+ #
76
+ # @return [SlsAdf::Util::Token] The reference to the default token used.
77
+ def token
78
+ @token ||= SlsAdf::Util::Token
79
+ end
80
+
81
+ # Reference to the GraphQL URL, which can be over-written.
82
+ #
83
+ # @return [String] The GraphQL URL
84
+ def graphql_url
85
+ @graphql_url ||= SlsAdf.configuration.graphql_url
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typhoeus'
4
+ require 'json'
5
+
6
+ module SlsAdf
7
+ module Util
8
+ # Token used to store the ADF API's client credentials token.
9
+ # This is used by +SlsAdf::Util::Adapter+ to make API calls to ADF.
10
+ #
11
+ # The token will be an empty string if:
12
+ # i) The get_token call is unsuccessful (HTTP status code not 200).
13
+ # ii) The credentials are invalid.
14
+ class Token
15
+ class << self
16
+ # Returns the token. If no token exists, an API call is made to get the token.
17
+ #
18
+ # @return [String] The ADF API token.
19
+ def token
20
+ @token ||= get_token
21
+ end
22
+
23
+ # Forces an API call to get the token.
24
+ #
25
+ # @return [String] The ADF API token.
26
+ def refresh_token
27
+ @token = get_token
28
+ end
29
+
30
+ private
31
+
32
+ # Returns the token after making an API call to get the token.
33
+ # An empty string is returned if:
34
+ # i) The call is unsuccessful (HTTP status code not 200).
35
+ # ii) The credentials are invalid.
36
+ #
37
+ # @return [String] The responded token, or an empty string.
38
+ def get_token # rubocop:disable Style/AccessorMethodName
39
+ response = get_token_call
40
+ response.code == 200 ? parse_response(response.body) : ''
41
+ end
42
+
43
+ # Returns the response for a POST token API call.
44
+ #
45
+ # @return [Typhoeus::Response] The response object.
46
+ def get_token_call # rubocop:disable Style/AccessorMethodName
47
+ Typhoeus.post(get_token_url, headers: COMMON_HEADERS, body: body)
48
+ end
49
+
50
+ # Attempts to parse the string in Json to obtain the token.
51
+ #
52
+ # @param [String] body String to be parsed
53
+ # @return [String] The parsed token, or blank if an error occurs.
54
+ def parse_response(body)
55
+ token = JSON.parse(body)['data']['token']
56
+ token ? token : ''
57
+ rescue JSON::ParserError
58
+ ''
59
+ end
60
+
61
+ def body
62
+ JSON.dump(
63
+ clientId: client_id,
64
+ clientSecret: client_secret,
65
+ grantType: 'client_credentials',
66
+ scope: 'all'
67
+ )
68
+ end
69
+
70
+ def client_id
71
+ SlsAdf.configuration.client_id
72
+ end
73
+
74
+ def client_secret
75
+ SlsAdf.configuration.client_secret
76
+ end
77
+
78
+ def get_token_url # rubocop:disable Style/AccessorMethodName
79
+ SlsAdf.configuration.get_token_url
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sls_adf/util/token'
4
+ require 'sls_adf/util/adapter'
5
+
6
+ module SlsAdf
7
+ module Util
8
+ COMMON_HEADERS = {
9
+ 'Accept' => 'application/json',
10
+ 'Content-Type' => 'application/json'
11
+ }.freeze
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlsAdf
4
+ VERSION = '0.0.1'
5
+ end
data/lib/sls_adf.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sls_adf/version'
4
+ require 'sls_adf/configuration'
5
+ require 'sls_adf/util'
6
+ require 'sls_adf/base'
7
+ require 'graphql/client'
8
+
9
+ module SlsAdf #:nodoc:
10
+ def self.adapter
11
+ @adapter ||= SlsAdf::Util::Adapter.new
12
+ end
13
+
14
+ def self.schema
15
+ @schema ||= begin
16
+ schema_path = File.join(File.dirname(__FILE__), "sls_adf/schema/schema.json")
17
+ GraphQL::Client.load_schema(schema_path)
18
+ end
19
+ end
20
+
21
+ def self.client
22
+ @client ||= GraphQL::Client.new(schema: schema, execute: adapter)
23
+ end
24
+ end
25
+
26
+ require 'sls_adf/query'
27
+ require 'sls_adf/mutation'
data/sls_adf.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ #sls_adf: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sls_adf/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'sls_adf'
8
+ s.version = SlsAdf::VERSION
9
+ s.authors = ['Toh Weiqing']
10
+ s.email = ['hello@estl.moe']
11
+
12
+ s.summary = 'Ruby client library for SLS (in progress).'
13
+ s.description = "Ruby client library for SLS's Application Development Framework (in progress)."
14
+ s.homepage = 'https://github.com/moexmen/sls-adf'
15
+ s.license = 'MIT'
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if s.respond_to?(:metadata)
20
+ s.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+ else
22
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
23
+ 'public gem pushes.'
24
+ end
25
+
26
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
27
+ f.match(%r{^(test|s|features)/})
28
+ end
29
+ s.bindir = 'exe'
30
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ s.require_paths = ['lib']
32
+ s.required_ruby_version = '>= 2.4.0'
33
+
34
+ s.add_dependency 'graphql-client', '~>0.12'
35
+ s.add_dependency 'typhoeus', '~>1.3'
36
+
37
+ s.add_development_dependency 'bundler', '~> 1.16'
38
+ s.add_development_dependency 'dotenv', '~> 2.2'
39
+ s.add_development_dependency 'rake', '~> 10.0'
40
+ s.add_development_dependency 'rdoc'
41
+ s.add_development_dependency 'rspec', '~> 3.0'
42
+ s.add_development_dependency 'simplecov'
43
+ s.add_development_dependency 'webmock'
44
+ end
@@ -0,0 +1,4 @@
1
+ if ENV['CI']
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
@@ -0,0 +1,66 @@
1
+ RSpec.describe SlsAdf::Base do
2
+ # Dummy class for testing SlsAdf::Base
3
+ class DummyBase < SlsAdf::Base
4
+ class << self
5
+ def test_execute_query(template, variables = {})
6
+ execute_query(template, variables)
7
+ end
8
+ end
9
+ end
10
+
11
+ module SlsAdf
12
+ # Set SlsAdf.client to point to configuration url, and load schema from there
13
+ def self.set_configration_sls_adf_client!
14
+ def self.client
15
+ @new_client ||= begin
16
+ new_schema = GraphQL::Client.load_schema(adapter)
17
+ GraphQL::Client.new(schema: new_schema, execute: adapter)
18
+ end
19
+ end
20
+ end
21
+
22
+ # Resets SlsAdf.client to the default
23
+ def self.reset_sls_adf_client!
24
+ def self.client
25
+ @client ||= GraphQL::Client.new(schema: schema, execute: adapter)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '.execute_query' do
31
+ subject { DummyBase.test_execute_query(template, variables) }
32
+ let(:character_variables) { { 'id' => '1001' } }
33
+ let(:starship_variables) { { 'id' => '3000' } }
34
+ before { SlsAdf.set_configration_sls_adf_client! }
35
+ after { SlsAdf.reset_sls_adf_client! }
36
+
37
+ it 'returns the correct response' do
38
+ WebMock.allow_net_connect!
39
+
40
+ GetCharacter = SlsAdf.client.parse <<~'GRAPHQL'
41
+ query ($id: ID!) {
42
+ character(id: $id) {
43
+ name
44
+ }
45
+ }
46
+ GRAPHQL
47
+
48
+ GetStarship = SlsAdf.client.parse <<~'GRAPHQL'
49
+ query($id: ID!) {
50
+ starship(id: $id) {
51
+ name
52
+ length
53
+ }
54
+ }
55
+ GRAPHQL
56
+
57
+ character_response = DummyBase.test_execute_query(GetCharacter, character_variables)
58
+ expect(character_response.data.to_h).to include('character' => hash_including('name' => 'Darth Vader'))
59
+
60
+ starship_response = DummyBase.test_execute_query(GetStarship, starship_variables)
61
+ expect(starship_response.data.to_h).to eq('starship' => { 'name' => 'Millenium Falcon', 'length' => 34.37 })
62
+
63
+ WebMock.disable_net_connect!
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ RSpec.describe SlsAdf::Configuration do
2
+ subject { SlsAdf.configuration }
3
+
4
+ it 'has a configuration object' do
5
+ expect(subject).not_to be nil
6
+ end
7
+
8
+ describe 'sls_adf_helper file' do
9
+ it 'initialises the paramters' do
10
+ expect(subject.graphql_url).not_to be_nil
11
+ expect(subject.get_token_url).not_to be_nil
12
+ expect(subject.client_id).not_to be_nil
13
+ expect(subject.client_secret).not_to be_nil
14
+ end
15
+ end
16
+
17
+ describe 'when editing parameters' do
18
+ let(:new_graphql_url) { 'new_url.com' }
19
+ let(:new_token_url) { 'new_url.com/token' }
20
+ let(:new_client_id) { 'new_client_id' }
21
+ let(:new_client_secret) { 'new_client_secret' }
22
+
23
+ before do
24
+ SlsAdf.configure do |config|
25
+ config.graphql_url = new_graphql_url
26
+ config.get_token_url = new_token_url
27
+ config.client_id = new_client_id
28
+ config.client_secret = new_client_secret
29
+ end
30
+ end
31
+ after { SlsAdf.initialise_sls_adf_gem! }
32
+
33
+ it 'changes the parameters when accessed again' do
34
+ expect(subject.graphql_url).to eq new_graphql_url
35
+ expect(subject.get_token_url).to eq new_token_url
36
+ expect(subject.client_id).to eq new_client_id
37
+ expect(subject.client_secret).to eq new_client_secret
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,81 @@
1
+ RSpec.describe SlsAdf::Mutation do
2
+ let!(:stub) do
3
+ stub_request(:post, SlsAdf.configuration.graphql_url).
4
+ to_return(status: 200, body: JSON.dump('data' => response_hash))
5
+ end
6
+ let(:uuid) { SecureRandom.uuid }
7
+
8
+ describe '.create_assignment' do
9
+ let(:input) do
10
+ {
11
+ 'title' => 'Foo',
12
+ 'start' => Time.now.iso8601,
13
+ 'end' => (Time.now + 10_000).iso8601,
14
+ 'createBy' => 'USERID-1',
15
+ 'type' => 'QUIZ',
16
+ 'subjectGroupUuid' => SecureRandom.uuid,
17
+ 'assignees' => ['USERID-2', 'USERID-3']
18
+ }
19
+ end
20
+ let(:response_hash) { { 'subjectGroup' => {} } }
21
+ subject { SlsAdf::Mutation.create_assignment(input) }
22
+
23
+ it 'makes a http request' do
24
+ subject
25
+ expect(stub).to have_been_requested
26
+ end
27
+ end
28
+
29
+ describe '.update_assignment' do
30
+ let(:input) { { 'title' => 'Bar' } }
31
+ let(:response_hash) { { 'subjectGroup' => {} } }
32
+ subject { SlsAdf::Mutation.update_assignment(uuid, input) }
33
+
34
+ it 'makes a http request' do
35
+ subject
36
+ expect(stub).to have_been_requested
37
+ end
38
+ end
39
+
40
+ describe '.delete_assignment' do
41
+ let(:response_hash) { { 'uuid' => {} } }
42
+ subject { SlsAdf::Mutation.delete_assignment(uuid) }
43
+
44
+ it 'makes a http request' do
45
+ subject
46
+ expect(stub).to have_been_requested
47
+ end
48
+ end
49
+
50
+ describe '.update_task' do
51
+ let(:uuid) { SecureRandom.uuid }
52
+ let(:status) { 'COMPLETED' }
53
+ let(:response_hash) { { 'task' => {} } }
54
+ subject { SlsAdf::Mutation.update_task(uuid, status) }
55
+
56
+ it 'makes a http request' do
57
+ subject
58
+ expect(stub).to have_been_requested
59
+ end
60
+ end
61
+
62
+ describe '.create_notification' do
63
+ let(:input) do
64
+ {
65
+ 'message' => 'Test notification',
66
+ 'scope' => 'SUBJECT_GROUP',
67
+ 'scopeId' => SecureRandom.uuid,
68
+ 'eventType' => 'LAUNCH_APP',
69
+ 'eventTypeId' => SecureRandom.uuid,
70
+ 'receipient' => ['USERID-1', 'USERID-2']
71
+ }
72
+ end
73
+ let(:response_hash) { { 'notification' => {} } }
74
+ subject { SlsAdf::Mutation.create_notification(input) }
75
+
76
+ it 'makes a http request' do
77
+ subject
78
+ expect(stub).to have_been_requested
79
+ end
80
+ end
81
+ end