From f03f2fd0685ae714c190ddb338d4bfc22d4a0263 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Tue, 5 Jan 2021 13:28:50 -0700 Subject: [PATCH] checkpoint --- Gemfile | 3 + Gemfile.lock | 13 + README.md | 8 + app/controllers/api/v1/swagger_controller.rb | 49 ++ app/controllers/api/v1/swagger_responses.rb | 64 +++ app/controllers/api/v1/users_swagger.rb | 531 +++++++++++++++++++ config/routes.rb | 2 + docker-compose.yml | 11 +- 8 files changed, 680 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/swagger_controller.rb create mode 100644 app/controllers/api/v1/swagger_responses.rb create mode 100644 app/controllers/api/v1/users_swagger.rb diff --git a/Gemfile b/Gemfile index 08bb5431..8f380031 100644 --- a/Gemfile +++ b/Gemfile @@ -151,6 +151,9 @@ gem 'bootsnap', '~> 1.4.0', require: false # Bulk inserts and upserts gem 'activerecord-import' +# Swagger API +gem "openstax_swagger", github: 'openstax/swagger-rails', ref: '9bff4962b31e142debbc62390f1fd3adab3af055' + group :development, :test do # Get env variables from .env file gem 'dotenv-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 169f22ad..74b57a2e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,17 @@ GIT json typhoeus (~> 1.0, >= 1.0.1) +GIT + remote: https://github.com/openstax/swagger-rails.git + revision: 9bff4962b31e142debbc62390f1fd3adab3af055 + ref: 9bff4962b31e142debbc62390f1fd3adab3af055 + specs: + openstax_swagger (0.1.0) + oj + oj_mimic_json + rails (~> 5.2.3) + swagger-blocks + GEM remote: https://rubygems.org/ specs: @@ -451,6 +462,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + swagger-blocks (3.0.0) test_xml (0.1.8) diffy (~> 3.0) nokogiri (>= 1.3.2) @@ -546,6 +558,7 @@ DEPENDENCIES openstax_api openstax_healthcheck openstax_rescue_from + openstax_swagger! openstax_utilities parallel_tests pg diff --git a/README.md b/README.md index 6edfc7f5..6ef60b97 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,14 @@ Then $> docker-compose run api bundle exec rspec ``` +If you update the Gemfile... + +```bash +$> docker-compose down +$> docker-compose run api bundle install +$> docker-compose up +``` + ### Install everything yourself 1. Install a ruby version manager on your machine, such as rbenv or rvm diff --git a/app/controllers/api/v1/swagger_controller.rb b/app/controllers/api/v1/swagger_controller.rb new file mode 100644 index 00000000..5da2a293 --- /dev/null +++ b/app/controllers/api/v1/swagger_controller.rb @@ -0,0 +1,49 @@ +require 'uri' + +class Api::V1::SwaggerController < OpenStax::Api::V1::ApiController + include ::Swagger::Blocks + + ACCEPT_HEADER = 'application/json' + BASE_PATH = '/api/v1' + + swagger_root do + key :swagger, '2.0' + info do + key :version, '1.0.0' + key :title, 'OpenStax Exercises API' + key :description, <<~DESC + The exercises API for OpenStax. + + Requests to this API should include `#{ACCEPT_HEADER}` in the `Accept` header. + + The desired API version is specified in the request URL, e.g. `[domain]#{BASE_PATH}/highlights`. \ + While the API does support a default version, that version will change over \ + time and therefore should not be used in production code! + DESC + key :termsOfService, 'https://openstax.org/tos' + contact do + key :name, 'support@openstax.org' + end + license do + key :name, 'MIT' + end + end + tag do + key :name, 'Exercises' + key :description, 'Exercises endpoints' + end + key :basePath, BASE_PATH + key :consumes, [ACCEPT_HEADER] + key :produces, ['application/json'] + end + + SWAGGERED_CLASSES = [ + Api::V1::SwaggerResponses, + Api::V1::UsersSwagger, + self, + ].freeze + + def json + render json: Swagger::Blocks.build_root_json(SWAGGERED_CLASSES) + end +end diff --git a/app/controllers/api/v1/swagger_responses.rb b/app/controllers/api/v1/swagger_responses.rb new file mode 100644 index 00000000..3dc6b434 --- /dev/null +++ b/app/controllers/api/v1/swagger_responses.rb @@ -0,0 +1,64 @@ +module Api::V0::SwaggerResponses + include Swagger::Blocks + include OpenStax::Swagger::SwaggerBlocksExtensions + + swagger_schema :Error do + property :status_code do + key :type, :integer + key :description, "The HTTP status code" + end + property :messages do + key :type, :array + key :description, "The error messages, if any" + items do + key :type, :string + end + end + end + + module AuthenticationError + def self.extended(base) + base.response 401 do + key :description, 'Not authenticated. The user is not authenticated.' + end + end + end + + module ForbiddenError + def self.extended(base) + base.response 403 do + key :description, 'Forbidden. The user is not allowed to perform the requested action.' + end + end + end + + module NotFoundError + def self.extended(base) + base.response 404 do + key :description, 'Not found' + end + end + end + + module UnprocessableEntityError + def self.extended(base) + base.response 422 do + key :description, 'Could not process request' + schema do + key :'$ref', :Error + end + end + end + end + + module ServerError + def self.extended(base) + base.response 500 do + key :description, 'Server error.' + schema do + key :'$ref', :Error + end + end + end + end +end diff --git a/app/controllers/api/v1/users_swagger.rb b/app/controllers/api/v1/users_swagger.rb new file mode 100644 index 00000000..05899d98 --- /dev/null +++ b/app/controllers/api/v1/users_swagger.rb @@ -0,0 +1,531 @@ +module Api::V1 + class UsersSwagger + include ::Swagger::Blocks + include OpenStax::Swagger::SwaggerBlocksExtensions + + DEFAULT_HIGHLIGHTS_PER_PAGE = 15 + MAX_HIGHLIGHTS_PER_PAGE = 200 + DEFAULT_HIGHLIGHTS_PAGE = 1 + + VALID_HIGHLIGHT_COLORS = %w(yellow green blue purple pink) + VALID_SETS = %w(user:me curated:openstax) + + swagger_schema :TextPositionSelector do + key :required, [:type, :start, :end] + property :start do + key :type, :string + key :description, 'The start to the text position selector.' + end + property :end do + key :type, :string + key :description, 'The end to the text position selector.' + end + property :type do + key :type, :string + key :description, 'The type for the text position selector.' + end + end + + swagger_schema :XpathRangeSelector do + key :required, [:end_container, :end_offset, :start_container, :start_offset, :type] + property :end_container do + key :type, :string + key :description, 'The end container for the xpath range selector.' + end + property :end_offset do + key :type, :integer + key :description, 'The end offset for the xpath range selector.' + end + property :start_container do + key :type, :string + key :description, 'The start container for the xpath range selector.' + end + property :start_offset do + key :type, :integer + key :description, 'The start offset for the xpath range selector.' + end + property :type do + key :type, :string + key :description, 'The type for the xpath range selector.' + end + end + + swagger_schema :Highlights do + # organization from https://jsonapi.org/ + property :meta do + property :page do + key :type, :integer + key :description, 'The current page number for these paginated results, one-indexed.' + end + property :per_page do + key :type, :integer + key :description, 'The requested number of results per page.' + end + property :count do + key :type, :integer + key :description, 'The number of results in the current page.' + end + property :total_count do + key :type, :integer + key :description, 'The number of results across all pages.' + end + end + property :data do + key :type, :array + key :description, 'The filtered highlights.' + items do + key :'$ref', :Highlight + end + end + end + + swagger_schema :ColorCount do + key :type, :object + end + add_properties(:ColorCount) do + property :yellow do + key :type, :integer + key :description, 'The count for yellow' + end + property :green do + key :type, :integer + key :description, 'The count for green' + end + property :blue do + key :type, :integer + key :description, 'The count for blue' + end + property :purple do + key :type, :integer + key :description, 'The count for purple' + end + property :pink do + key :type, :integer + key :description, 'The count for pink' + end + end + + swagger_schema :HighlightsSummary do + property :counts_per_source do + key :type, :object + key :description, 'Map of source ID to number of highlights by color in that source' + key :additionalProperties, { + '$ref' => '#/definitions/ColorCount', + } + end + end + + COMMON_REQUIRED_HIGHLIGHT_FIELDS = [ + :source_type, :source_id, :anchor, :highlighted_content, :color, :location_strategies + ] + + swagger_schema :Highlight do + key :required, COMMON_REQUIRED_HIGHLIGHT_FIELDS | [:id] + end + + swagger_schema :NewHighlight do + key :required, COMMON_REQUIRED_HIGHLIGHT_FIELDS + end + + add_properties(:Highlight, :NewHighlight) do + property :id do + key :type, :string + key :format, 'uuid' + key :description, 'The highlight ID.' + end + property :source_type do + key :type, :string + key :enum, ['openstax_page'] + key :description, 'The type of content that contains the highlight.' + end + property :source_id do + key :type, :string + key :pattern, /^[^,]+$/ + key :description, 'The ID of the source document in which the highlight is made. ' \ + 'Has source_type-specific constraints (e.g. all lowercase UUID for ' \ + 'the \'openstax_page\' source_type). Because source_ids are passed ' \ + 'to query endpoints as comma-separated values, they cannot contain ' \ + 'commas.' + end + property :scope_id do + key :type, :string + key :description, 'The ID of the container for the source in which the highlight is made. ' \ + 'Varies depending on source_type (e.g. is the lowercase, versionless ' \ + 'book UUID for the \'openstax_page\' source_type).' + end + property :prev_highlight_id do + key :type, :string + key :format, 'uuid' + key :description, 'The ID of the highlight immediately before this highlight. May be ' \ + 'null if there are no preceding highlights in this source.' + end + property :next_highlight_id do + key :type, :string + key :format, 'uuid' + key :description, 'The ID of the highlight immediately after this highlight. May be ' \ + 'null if there are no following highlights in this source.' + end + property :color do + key :type, :string + key :enum, VALID_HIGHLIGHT_COLORS + key :description, 'The name of the highlight color. Corresponding RGB values for ' \ + 'different states (e.g. focused, passive) are maintained in the ' \ + 'client.' + end + property :anchor do + key :type, :string + key :description, 'The anchor of the highlight.' + end + property :highlighted_content do + key :type, :string + key :description, 'The highlighted content.' + end + property :annotation do + key :type, :string + key :description, 'The note attached to the highlight.' + end + property :location_strategies do + key :type, :array + key :description, "Location strategies for the highlight. Items should have a schema " \ + "matching the strategy schemas that have been defined. (" \ + "`XpathRangeSelector` or `TextPositionSelector`)." + items do + key :type, :object + end + end + end + + add_properties(:Highlight) do + property :order_in_source do + key :type, :number + key :readOnly, true + key :description, 'A number whose relative value gives the highlight\'s order within the ' \ + 'source. Its value has no meaning on its own.' + end + end + + swagger_schema :HighlightUpdate do + property :color do + key :type, :string + key :enum, VALID_HIGHLIGHT_COLORS + key :description, 'The new name of the highlight color. Corresponding RGB values for ' \ + 'different states (e.g. focused, passive) are maintained in the ' \ + 'client.' + end + property :annotation do + key :type, :string + key :description, 'The new note for the highlight (replaces existing note).' + end + end + + swagger_path '/highlights' do + operation :post do + key :summary, 'Add a highlight' + key :description, 'Add a highlight with an optional note' + key :operationId, 'addHighlight' + key :produces, [ + 'application/json' + ] + key :tags, [ + 'Highlights' + ] + parameter do + key :name, :highlight + key :in, :body + key :description, 'The highlight data' + key :required, true + schema do + key :'$ref', :NewHighlight + end + end + response 201 do + key :description, 'Created. Returns the created highlight.' + schema do + key :'$ref', :Highlight + end + end + extend Api::V0::SwaggerResponses::AuthenticationError + extend Api::V0::SwaggerResponses::UnprocessableEntityError + extend Api::V0::SwaggerResponses::ServerError + end + end + + swagger_path_and_parameters_schema '/highlights' do + operation :get do + key :summary, 'Get filtered highlights' + key :description, <<~DESC + Get filtered highlights belonging to the calling user. + + Highlights can be filtered thru query parameters: source_type, scope_id, source_ids, \ + and color. + + Results are paginated and ordered. When source_ids are specified, the order is order \ + within the sources. When source_ids are not specified, the order is by creation time. + + Example call: + /api/v0/highlights?source_type=openstax_page&scope_id=123&color=#{VALID_HIGHLIGHT_COLORS.first} + DESC + key :operationId, 'getHighlights' + key :produces, [ + 'application/json' + ] + key :tags, [ + 'Highlights' + ] + parameter do + key :name, :source_type + key :in, :query + key :type, :string + key :required, true + key :enum, ['openstax_page'] + key :description, 'Limits results to those highlights made in sources of this type.' + end + parameter do + key :name, :scope_id + key :in, :query + key :type, :string + key :required, false + key :description, 'Limits results to the source document container in which the highlight ' \ + 'was made. For openstax_page source_types, this is a versionless book UUID. ' \ + 'If this is not specified, results across scopes will be returned, meaning ' \ + 'the order of the results will not be meaningful.' + end + parameter do + key :name, :sets + key :in, :query + key :type, :array + key :collectionFormat, :csv + key :required, false + key :description, 'One or more sets to load data from; default is "user:me"' + items do + key :type, :string + key :enum, VALID_SETS + end + end + parameter do + key :name, :source_ids + key :in, :query + key :type, :array + key :collectionFormat, :csv + key :required, false + key :description, 'One or more source IDs; query results will contain highlights ordered '\ + 'by the order of these source IDs and ordered within each source. If ' \ + 'parameter is an empty array, no results will be returned. If the ' \ + 'parameter is not provided, all highlights under the scope will be ' \ + 'returned.' + items do + key :type, :string + end + end + parameter do + key :name, :colors + key :in, :query + key :type, :array + key :collectionFormat, :csv + key :required, false + key :description, 'Limits results to these highlight colors.' + items do + key :type, :string + key :enum, VALID_HIGHLIGHT_COLORS + end + end + parameter do + key :name, :page + key :in, :query + key :type, :integer + key :required, false + key :description, 'The page number of paginated results, one-indexed.' + key :minimum, 1 + key :default, DEFAULT_HIGHLIGHTS_PAGE + end + parameter do + key :name, :per_page + key :in, :query + key :type, :integer + key :required, false + key :description, 'The number of highlights per page for paginated results.' + key :minimum, 0 + key :maximum, MAX_HIGHLIGHTS_PER_PAGE + key :default, DEFAULT_HIGHLIGHTS_PER_PAGE + end + response 200 do + key :description, 'Success. Returns the filtered highlights.' + schema do + key :'$ref', :Highlights + end + end + extend Api::V0::SwaggerResponses::AuthenticationError + extend Api::V0::SwaggerResponses::UnprocessableEntityError + extend Api::V0::SwaggerResponses::ServerError + end + end + + swagger_path_and_parameters_schema '/highlights/summary' do + operation :get do + key :summary, 'Get summary of highlights (counts per source, etc)' + key :description, <<~DESC + Get summary of highlights (counts per source, etc) belonging to the calling user. + + Highlights can be filtered thru query parameters: source_type, scope_id, and color. + + Results are not paginated. + + Example call: + /api/v0/highlights/summary?source_type=openstax_page&scope_id=123&color=#ff0000 + DESC + key :operationId, 'getHighlightsSummary' + key :produces, [ + 'application/json' + ] + key :tags, [ + 'Highlights' + ] + parameter do + key :name, :source_type + key :in, :query + key :type, :string + key :required, true + key :enum, ['openstax_page'] + key :description, 'Limits summary to those highlights made in sources of this type.' + end + parameter do + key :name, :scope_id + key :in, :query + key :type, :string + key :required, false + key :description, 'Limits summary to the source document container in which the highlights ' \ + 'were made. For openstax_page source_types, this is a versionless book UUID.' + end + parameter do + key :name, :sets + key :in, :query + key :type, :array + key :collectionFormat, :csv + key :required, false + key :description, 'One or more sets to load data from; default is "user:me"' + items do + key :type, :string + key :enum, VALID_SETS + end + end + parameter do + key :name, :colors + key :in, :query + key :type, :array + key :collectionFormat, :csv + key :required, false + key :description, 'Limits results to these highlight colors.' + items do + key :type, :string + key :enum, VALID_HIGHLIGHT_COLORS + end + end + response 200 do + key :description, 'Success. Returns the summary.' + schema do + key :'$ref', :HighlightsSummary + end + end + extend Api::V0::SwaggerResponses::AuthenticationError + extend Api::V0::SwaggerResponses::UnprocessableEntityError + extend Api::V0::SwaggerResponses::ServerError + end + end + + swagger_path '/highlights/{id}' do + operation :get do + key :summary, 'Get a highlight without its annotation data.' + key :description, 'Get a highlight without its annotation data.' + key :operationId, 'getHighlight' + key :produces, [ + 'application/json' + ] + key :tags, [ + 'Highlights' + ] + parameter do + key :name, :id + key :in, :path + key :description, 'ID of the highlight to find.' + key :required, true + key :type, :string + key :format, 'uuid' + end + response 200 do + key :description, 'Success. Returns the queried highlight.' + schema do + key :'$ref', :Highlight + end + end + extend Api::V0::SwaggerResponses::UnprocessableEntityError + extend Api::V0::SwaggerResponses::ServerError + end + end + + swagger_path '/highlights/{id}' do + operation :put do + key :summary, 'Update a highlight' + key :description, 'Update a highlight' + key :operationId, 'updateHighlight' + key :produces, [ + 'application/json' + ] + key :tags, [ + 'Highlights' + ] + parameter do + key :name, :id + key :in, :path + key :description, 'ID of the highlight to update.' + key :required, true + key :type, :string + key :format, 'uuid' + end + parameter do + key :name, :highlight + key :in, :body + key :description, 'The highlight updates.' + key :required, true + schema do + key :'$ref', :HighlightUpdate + end + end + response 200 do + key :description, 'Success. Returns the updated highlight.' + schema do + key :'$ref', :Highlight + end + end + extend Api::V0::SwaggerResponses::AuthenticationError + extend Api::V0::SwaggerResponses::UnprocessableEntityError + extend Api::V0::SwaggerResponses::ServerError + end + end + + swagger_path '/highlights/{id}' do + operation :delete do + key :summary, 'Delete a highlight' + key :description, 'Delete a highlight. Can only be done by the owner of the highlight.' + key :operationId, 'deleteHighlight' + key :tags, [ + 'Highlights' + ] + parameter do + key :name, :id + key :in, :path + key :description, 'ID of the highlight to delete' + key :required, true + key :type, :string + key :format, 'uuid' + end + response 200 do + key :description, 'Deleted.' + end + extend Api::V0::SwaggerResponses::AuthenticationError + extend Api::V0::SwaggerResponses::ForbiddenError + extend Api::V0::SwaggerResponses::NotFoundError + extend Api::V0::SwaggerResponses::ServerError + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 848dc743..603cd7c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,6 +47,8 @@ resources :users, only: [:index] resource :user, only: [:show, :update, :destroy] + + get :swagger, to: 'swagger#json' end mount OpenStax::Accounts::Engine => :accounts diff --git a/docker-compose.yml b/docker-compose.yml index fa8df358..8df58c39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +# bundle cache method: https://dev.to/k_penguin_sato/cache-rails-gems-using-docker-compose-3o3f + version: "3.7" services: api: @@ -6,7 +8,7 @@ services: - exercises ports: - "3000:3000" - command: bash -c "bin/rake about && bin/rails server -b '0.0.0.0' -p 3000" + command: bash -c "bundle exec rails server -b '0.0.0.0' -p 3000" environment: - OXE_DEV_DB_HOST=postgres - OXE_TEST_DB_HOST=postgres_test @@ -15,10 +17,15 @@ services: - OXE_DEV_DB=ox_exercises_dev - OXE_TEST_DB=ox_exercises_test - REDIS_URL=redis://redis:6379/0 + - BUNDLE_PATH=/bundle/vendor volumes: - .:/code - /code/tmp - /code/log + - bundle_path:/bundle + depends_on: + - postgres + - postgres_test postgres: image: "postgres:9.5" volumes: @@ -49,6 +56,8 @@ services: networks: exercises: + volumes: pgdata: redis: + bundle_path: