sls_adf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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