psc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +13 -0
  2. data/.rvmrc +2 -0
  3. data/.yardopts +4 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +20 -0
  6. data/LICENSE +20 -0
  7. data/README.md +163 -0
  8. data/Rakefile +69 -0
  9. data/cucumber.yml +13 -0
  10. data/features/step_definitions/eval_steps.rb +14 -0
  11. data/features/step_definitions/setup_steps.rb +7 -0
  12. data/features/studies.feature +21 -0
  13. data/features/support/env.rb +33 -0
  14. data/features/support/int_psc.rb +165 -0
  15. data/int-psc/README-int-psc.md +57 -0
  16. data/int-psc/hsqldb/baseline.log +197 -0
  17. data/int-psc/hsqldb/baseline.properties +17 -0
  18. data/int-psc/hsqldb/baseline.script +295 -0
  19. data/int-psc/hsqldb/datasource.properties +18 -0
  20. data/int-psc/hsqldb/datasource.script +2205 -0
  21. data/int-psc/jetty/jetty-runner-7.4.0.v20110414.jar +0 -0
  22. data/int-psc/state/ABC 1200.xml +115 -0
  23. data/int-psc/state/int-psc-state.xml +64 -0
  24. data/lib/psc.rb +47 -0
  25. data/lib/psc/client.rb +35 -0
  26. data/lib/psc/connection.rb +96 -0
  27. data/lib/psc/faraday.rb +11 -0
  28. data/lib/psc/faraday/http_basic.rb +35 -0
  29. data/lib/psc/faraday/psc_token.rb +42 -0
  30. data/lib/psc/faraday/string_is_xml.rb +25 -0
  31. data/lib/psc/version.rb +3 -0
  32. data/meta.rakefile +30 -0
  33. data/psc.gemspec +33 -0
  34. data/spec/fixtures/studies-json.http +37 -0
  35. data/spec/middleware_helper.rb +18 -0
  36. data/spec/psc/client_spec.rb +39 -0
  37. data/spec/psc/connection_spec.rb +156 -0
  38. data/spec/psc/faraday/http_basic_spec.rb +15 -0
  39. data/spec/psc/faraday/psc_token_spec.rb +38 -0
  40. data/spec/psc/faraday/string_is_xml_spec.rb +49 -0
  41. data/spec/psc/version_spec.rb +11 -0
  42. data/spec/psc_spec.rb +16 -0
  43. data/spec/spec_helper.rb +15 -0
  44. data/tasks/int-psc.rake +84 -0
  45. data/tasks/psc/TODO +3 -0
  46. data/tasks/psc/state.rb +393 -0
  47. metadata +311 -0
@@ -0,0 +1,13 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ reports
6
+
7
+ int-psc/downloads
8
+ int-psc/bin/*
9
+ int-psc/deploy-base
10
+ int-psc/baseline.properties
11
+ int-psc/datasource.properties
12
+
13
+ .yardoc
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm_gemset_create_on_use_flag=1; rvm use @psc-rb
2
+
@@ -0,0 +1,4 @@
1
+ --no-private
2
+ --markup markdown
3
+ --hide-void-return
4
+ --files CHANGELOG.md,int-psc/README-int-psc.md
@@ -0,0 +1,4 @@
1
+ 0.0.1
2
+ =====
3
+
4
+ - Initial version
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "http://rubygems.org/"
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ # For yard's markdown support
7
+ platforms :jruby do
8
+ gem 'maruku'
9
+ end
10
+
11
+ platforms :ruby_18, :ruby_19 do
12
+ gem 'rdiscount'
13
+ end
14
+
15
+ # To prevent jruby problems in development
16
+ platforms :jruby do
17
+ gem 'jruby-openssl' # when using webmock
18
+ gem 'ffi-ncurses' # when loading highline
19
+ end
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Rhett Sutphin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,163 @@
1
+ (Beware: [Readme-driven development in
2
+ progress](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html). This
3
+ is documentation for expected features, not for the current state of
4
+ the library.)
5
+
6
+ # psc.rb
7
+
8
+ `psc.rb` is a ruby client for [Patient Study Calendar][psc]'s RESTful
9
+ HTTP API. It provides assistance with authentication to PSC's API and
10
+ with executing common tasks. It also provides a lower-level interface
11
+ (`Psc::Connection`) to allow for making HTTP requests against the
12
+ configured PSC's API directly.
13
+
14
+ By design, the client provides a very thin abstraction over the API
15
+ itself. Please be familiar with the API (whose documentation is
16
+ available in your PSC instance at `api/v1/docs` or [on the demo
17
+ site][demo-docs]) before using this library.
18
+
19
+ [psc]: https://code.bioinformatics.northwestern.edu/issues/wiki/psc
20
+ [demo-docs]: https://demos.nubic.northwestern.edu/psc/api/v1/docs
21
+
22
+ ## Overview
23
+
24
+ require 'psc'
25
+ require 'pp'
26
+
27
+ psc = Psc::Client.new(
28
+ 'https://demos.nubic.northwestern.edu/psc',
29
+ :authenticator => { :basic => ['superuser', 'superuser'] }
30
+ )
31
+
32
+ pp psc.studies
33
+
34
+ (This code will run if you have the psc gem installed; try it and see.)
35
+
36
+ ## Installing
37
+
38
+ `psc.rb` is available as a rubygem:
39
+
40
+ $ gem install psc
41
+
42
+ ## Authentication
43
+
44
+ PSC supports two forms of authentication for API calls: HTTP Basic
45
+ (i.e., username & password) and psc_token. (Which forms are supported
46
+ in your PSC instance will depend on its authentication system
47
+ configuration.)
48
+
49
+ A particular client instance will only use one authentication
50
+ mechanism. There are three options.
51
+
52
+ ### HTTP Basic
53
+
54
+ PSC Client allows you to specify a username and password to use for
55
+ all requests. Include the `:authenticator` key like so:
56
+
57
+ :authenticator => { :basic => %w(alice password) }
58
+
59
+ => Authorization: Basic YWxpY2U6cGFzc3dvcmQ=
60
+
61
+ ### Static token
62
+
63
+ Alternatively, you can provide a token to use in all requests:
64
+
65
+ :authenticator => { :token => 'The raven flies at midnight' }
66
+
67
+ => Authorization: psc_token The raven flies at midnight
68
+
69
+ ### Dynamic token
70
+
71
+ Finally, you can provide a callable object which will be invoked for
72
+ each request and whose return value will be used for the PSC token:
73
+
74
+ :authenticator => { :token => lambda { cas_client.get_proxy_ticket } }
75
+
76
+ => Authorization: psc_token PT-133-236H522
77
+
78
+ The callable will be called with no arguments.
79
+
80
+ ## High-level interface
81
+
82
+ {Psc::Client} provides a high-level interface to some of PSC's API
83
+ capabilities.
84
+
85
+ ## Low-level interface
86
+
87
+ `psc.rb` is based on [Faraday][], a modular ruby HTTP
88
+ client. {Psc::Connection} is a Faraday connection configured
89
+ for access to a particular PSC instance. You can create a
90
+ `Psc::Connection` directly:
91
+
92
+ conn = Psc::Connection.new(
93
+ 'https://demos.nubic.northwestern.edu/psc',
94
+ :authenticator => { :basic => %w(superuser superuser) })
95
+
96
+ Or you can get an instance from the {Psc::Client} high-level
97
+ interface:
98
+
99
+ client = Psc::Client.new(
100
+ 'https://demos.nubic.northwestern.edu/psc',
101
+ :authenticator => { :basic => %w(superuser superuser) })
102
+ conn = client.connection
103
+
104
+ The connection is set up to automatically parse JSON reponses into
105
+ appropriate ruby primitives and XML responses into [Nokogiri][]
106
+ documents.
107
+
108
+ studies_json = conn.get('studies.json')
109
+ first_study_name =
110
+ studies_json.body['studies'].first['assigned_identifier']
111
+
112
+ sites_xml = conn.get('sites.xml')
113
+ first_site_name =
114
+ sites_xml.body.xpath('//psc:site', Psc.xml_namespace).first.attr('site-name')
115
+
116
+ Similarly, for PUT and POST it will encode a `Hash` or
117
+ `Array` entity as JSON and will assume that a `String` entity is XML.
118
+
119
+ [Faraday]: https://github.com/technoweenie/faraday
120
+ [Nokogiri]: http://nokogiri.org/
121
+
122
+ ### URLs
123
+
124
+ PSC's API resources all start with `api/v1`. To help you DRY things
125
+ up, `PSC::Connection` automatically adds this to the base URL on
126
+ construction. You don't need to include it when constructing
127
+ relative URLs.
128
+
129
+ ### Middleware
130
+
131
+ Faraday connections are built up from middleware. `Psc::Connection`
132
+ uses a combination of off-the-shelf and custom middleware classes. The
133
+ custom classes are in the {Psc::Faraday} module.
134
+
135
+ ## Project info
136
+
137
+ * [Source code][github]
138
+ * [API Documentation][rubydoc]
139
+ * [Bug reports and feature requests][issues]
140
+ * [Continuous integration][ci]
141
+ * Version policy: [semantic versioning][semver]
142
+
143
+ [rubydoc]: http://rubydoc.info/gems/psc
144
+ [issues]: https://github.com/NUBIC/psc.rb/issues
145
+ [github]: https://github.com/NUBIC/psc.rb
146
+ [ci]: https://ctms-ci.nubic.northwestern.edu/hudson/jobs/psc.rb
147
+ [semver]: http://semver.org/
148
+
149
+ ### Running the tests
150
+
151
+ `psc.rb` has an rspec suite for unit tests and a set of cucumber
152
+ features for integration tests. Before you can run the cucumber
153
+ features you will need to either
154
+
155
+ * Execute `rake int-psc:war` to download a copy of PSC to use
156
+ * Copy a PSC war into `int-psc/bin`
157
+
158
+ ### Patches
159
+
160
+ Patches with tests are happily considered. Please use a [pull
161
+ request][].
162
+
163
+ [pull request]: http://help.github.com/pull-requests/
@@ -0,0 +1,69 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'ci/reporter/rake/rspec'
6
+ require 'cucumber/rake/task'
7
+ require 'yard'
8
+
9
+ Dir[File.expand_path('../tasks/*.rake', __FILE__)].each { |f| load f }
10
+
11
+ desc "Run all specs"
12
+ RSpec::Core::RakeTask.new('spec') do |t|
13
+ t.pattern = 'spec/**/*_spec.rb'
14
+ t.verbose = true
15
+ t.rspec_opts = ['--format', 'html', '--out', 'reports/spec.html', '--format', 'p']
16
+ end
17
+
18
+ task :cucumber => 'cucumber:ok'
19
+
20
+ namespace :cucumber do
21
+ Cucumber::Rake::Task.new(:ok, "Run features that should pass") do |t|
22
+ t.fork = true
23
+ t.profile = "default"
24
+ end
25
+
26
+ Cucumber::Rake::Task.new(:wip, "Run features that are being worked on") do |t|
27
+ t.fork = true
28
+ t.profile = "wip"
29
+ end
30
+
31
+ Cucumber::Rake::Task.new(
32
+ :wip_platform, "Run features that are flagged as failing on the current platform"
33
+ ) do |t|
34
+ t.fork = true
35
+ t.profile = "wip_platform"
36
+ end
37
+
38
+ desc "Run all features"
39
+ task :all => [:ok, :wip, :wip_platform]
40
+ end
41
+
42
+ task :yard => ['yard:auto']
43
+
44
+ namespace :yard do
45
+ desc "Run a server which will rebuild documentation as the source changes"
46
+ task :auto do
47
+ system("bundle exec yard server --reload")
48
+ end
49
+
50
+ desc "Build API documentation with yard"
51
+ YARD::Rake::YardocTask.new("once") do |t|
52
+ t.options = ["--title", "psc.rb #{Psc::VERSION}"]
53
+ end
54
+ end
55
+
56
+ namespace :ci do
57
+ ENV["CI_REPORTS"] = "reports/spec-xml"
58
+
59
+ desc "Run specs for CI"
60
+ task :spec => ['ci:setup:rspec', 'rake:spec']
61
+
62
+ Cucumber::Rake::Task.new(:cucumber, 'Run features using the ci profile') do |t|
63
+ t.fork = true
64
+ t.profile = 'ci'
65
+ end
66
+
67
+ # currently bypassing ci_reporter due to https://github.com/nicksieger/ci_reporter/issues/5
68
+ task :build => %w(int-psc:war int-psc:clean_logs rake:spec cucumber)
69
+ end
@@ -0,0 +1,13 @@
1
+ <% platform =
2
+ if RUBY_PLATFORM == 'java'
3
+ 'jruby'
4
+ elsif RUBY_VERSION =~ /^1.9/
5
+ '19'
6
+ else
7
+ '18'
8
+ end
9
+ %>
10
+ default: --strict --tags ~@wip --tags ~@no_<%= platform %> features
11
+ wip: --tags @wip:3 --wip --tags ~@no_<%= platform %> features
12
+ ci: --tags ~@wip --tags ~@badci --tags ~@no_<%= platform %> --format html --out reports/cucumber.html --format junit --out reports/cucumber-xml --format progress --strict features
13
+ wip_platform: --tags @no_<%= platform %> --wip features
@@ -0,0 +1,14 @@
1
+ When /^I evaluate the following code:$/ do |string|
2
+ int_psc.wait_for
3
+ @captured_out = StringIO.new
4
+ begin
5
+ $stdout = @captured_out
6
+ eval(string)
7
+ ensure
8
+ $stdout = STDOUT
9
+ end
10
+ end
11
+
12
+ Then /^I should see this output:$/ do |string|
13
+ @captured_out.string.strip.should == string
14
+ end
@@ -0,0 +1,7 @@
1
+ Given /^that PSC is deployed$/ do
2
+ int_psc.boot
3
+ end
4
+
5
+ Given /^I have a PSC::Client instance$/ do
6
+ init_client
7
+ end
@@ -0,0 +1,21 @@
1
+ Feature: List studies
2
+
3
+ In order to understand what templates are available
4
+ A developer should be able to use Psc::Client to list the studies in a PSC instance
5
+
6
+ Background:
7
+ Given that PSC is deployed
8
+ And I have a PSC::Client instance
9
+
10
+ Scenario: List studies
11
+ When I evaluate the following code:
12
+ """
13
+ s = client.studies
14
+ puts "Study count: #{s.size}"
15
+ puts "First study: #{s.first['assigned_identifier']}"
16
+ """
17
+ Then I should see this output:
18
+ """
19
+ Study count: 1
20
+ First study: ABC 1200
21
+ """
@@ -0,0 +1,33 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+
4
+ require 'rspec'
5
+
6
+ $LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
7
+
8
+ require 'psc'
9
+
10
+ module Psc::Cucumber
11
+ include ::RSpec::Matchers
12
+
13
+ class World
14
+ def int_psc
15
+ @int_psc ||= IntPsc.new
16
+ end
17
+
18
+ def init_client
19
+ @client = Psc::Client.new(
20
+ File.join(IntPsc.url, 'api/v1'),
21
+ :authenticator => { :basic => [ 'superuser' ] * 2 }
22
+ )
23
+ end
24
+
25
+ def client
26
+ @client
27
+ end
28
+ end
29
+ end
30
+
31
+ World do
32
+ Psc::Cucumber::World.new
33
+ end
@@ -0,0 +1,165 @@
1
+ # Utilities for starting and stopping a PSC instance for integration testing
2
+
3
+ require 'childprocess'
4
+ require 'open-uri'
5
+ require 'timeout'
6
+ require 'forwardable'
7
+ require 'fileutils'
8
+
9
+ class IntPsc
10
+ DEFAULT_CONFIGURATION_NAME='datasource'
11
+
12
+ extend Forwardable
13
+ include FileUtils
14
+
15
+ def self.path(*bits)
16
+ File.join(File.expand_path('../../..', __FILE__), 'int-psc', *bits)
17
+ end
18
+ def_delegator self, :path
19
+
20
+ def self.port
21
+ ENV['INT_PSC_PORT'] ||= '7209'
22
+ end
23
+ def_delegator self, :port
24
+
25
+ def self.url
26
+ "http://localhost:#{port}/"
27
+ end
28
+ def_delegator self, :url
29
+
30
+ def self.warfile
31
+ path('bin', 'psc.war')
32
+ end
33
+ def_delegator self, :warfile
34
+
35
+ def self.expanded_war_directory
36
+ path('deploy-base', 'webapps', 'ROOT')
37
+ end
38
+ def_delegator self, :expanded_war_directory
39
+
40
+ def self.run(configuration=DEFAULT_CONFIGURATION_NAME)
41
+ psc = self.new(configuration)
42
+ psc.boot
43
+ puts "Waiting for PSC's API to become available"
44
+ psc.wait_for
45
+ yield psc
46
+ psc.stop
47
+ end
48
+
49
+ attr_reader :configuration_name
50
+
51
+ def initialize(configuration=DEFAULT_CONFIGURATION_NAME)
52
+ @configuration_name = configuration
53
+ end
54
+
55
+ def boot
56
+ expand_if_necessary
57
+ create_hsql_psc_configuration
58
+
59
+ cmd = [
60
+ 'java',
61
+ ENV['JAVA_OPTS'],
62
+ "-Dpsc.config.datasource=#{configuration_name}",
63
+ "-Dpsc.config.path=#{path}",
64
+ '-Dpsc.logging.debug=true',
65
+ "-Dcatalina.base=#{path 'deploy-base'}",
66
+
67
+ '-jar',
68
+ # jetty-runner
69
+ path('jetty', 'jetty-runner-7.4.0.v20110414.jar'),
70
+ "--port", port,
71
+ expanded_war_directory
72
+ ].compact
73
+
74
+ @running_psc = ChildProcess.build(*cmd)
75
+ @running_psc.duplex = true
76
+
77
+ @output_capture = rolled_log('jetty.out')
78
+ @running_psc.io.stdout = @output_capture
79
+ @running_psc.io.stderr = @output_capture
80
+
81
+ at_exit { self.stop }
82
+ @running_psc.start
83
+ @running_psc.io.stdin.close # to turn off ShellTUI for the OSGi layer
84
+ end
85
+
86
+ def stop
87
+ if @running_psc && @running_psc.alive?
88
+ @running_psc.stop
89
+ @output_capture.close
90
+ end
91
+ end
92
+
93
+ def wait_for
94
+ Timeout.timeout(90) do
95
+ while true
96
+ begin
97
+ open(File.join(self.url, '/api/v1/docs')) { |f| f.read }
98
+ break
99
+ rescue RuntimeError
100
+ sleep(1)
101
+ rescue
102
+ sleep(1)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def create_hsql_psc_configuration
109
+ File.open(path("#{configuration_name}.properties"), 'w') do |f|
110
+ f.puts [
111
+ "datasource.url=jdbc:hsqldb:file:#{path('hsqldb', configuration_name)};shutdown=true",
112
+ 'datasource.username=sa',
113
+ 'datasource.password=',
114
+ 'datasource.driver=org.hsqldb.jdbcDriver'
115
+ ].join("\n")
116
+ end
117
+ end
118
+
119
+ def apply_state_and_mark_readonly
120
+ client = Faraday.new(File.join(url, '/api/v1')) do |builder|
121
+ builder.adapter :net_http
122
+ end
123
+ client.basic_auth('superuser', 'superuser')
124
+ Psc::State.from_file(path('state/int-psc-state.xml')).apply(client)
125
+
126
+ File.open(path('hsqldb', "#{configuration_name}.properties"), 'a') do |f|
127
+ f.puts 'hsqldb.files_readonly=true'
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def expand_if_necessary
134
+ dirtime = File.directory?(expanded_war_directory) ?
135
+ File.mtime(expanded_war_directory) :
136
+ Time.at(0)
137
+ wartime = File.mtime(warfile)
138
+ if wartime > dirtime
139
+ rm_rf expanded_war_directory
140
+ mkdir_p expanded_war_directory
141
+ cd expanded_war_directory do
142
+ system("jar xf '#{warfile}'")
143
+ end
144
+ end
145
+ end
146
+
147
+ def rolled_log(name)
148
+ log = path('deploy-base', 'logs', name)
149
+ mkdir_p File.dirname(log)
150
+ if File.exist?(log)
151
+ now_s = date_str_from_time(Time.new)
152
+ existing_s = date_str_from_time(File.mtime(log))
153
+ if now_s != existing_s
154
+ rolled_log_name = log.sub(/\.([^\.]+)$/, ".#{existing_s}.\\1")
155
+ mv log, rolled_log_name
156
+ system("gzip -N '#{rolled_log_name}'")
157
+ end
158
+ end
159
+ File.open(log, 'a')
160
+ end
161
+
162
+ def date_str_from_time(t)
163
+ t.iso8601.split('T').first
164
+ end
165
+ end