alma 0.2.6 → 0.3.3
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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +54 -0
- data/.circleci/setup-rubygems.sh +3 -0
- data/.github/dependabot.yml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +134 -0
- data/.ruby-version +1 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +4 -3
- data/Guardfile +75 -0
- data/README.md +136 -26
- data/Rakefile +3 -1
- data/alma.gemspec +21 -16
- data/lib/alma/alma_record.rb +3 -3
- data/lib/alma/api_defaults.rb +39 -0
- data/lib/alma/availability_response.rb +50 -53
- data/lib/alma/bib.rb +26 -42
- data/lib/alma/bib_holding.rb +25 -0
- data/lib/alma/bib_item.rb +28 -38
- data/lib/alma/bib_item_set.rb +72 -12
- data/lib/alma/bib_set.rb +7 -21
- data/lib/alma/config.rb +10 -4
- data/lib/alma/course.rb +47 -0
- data/lib/alma/course_set.rb +17 -0
- data/lib/alma/electronic/README.md +20 -0
- data/lib/alma/electronic/batch_utils.rb +224 -0
- data/lib/alma/electronic/business.rb +29 -0
- data/lib/alma/electronic.rb +167 -0
- data/lib/alma/error.rb +16 -4
- data/lib/alma/fine.rb +16 -0
- data/lib/alma/fine_set.rb +36 -21
- data/lib/alma/item_request_options.rb +23 -0
- data/lib/alma/library.rb +29 -0
- data/lib/alma/library_set.rb +21 -0
- data/lib/alma/loan.rb +31 -2
- data/lib/alma/loan_set.rb +62 -15
- data/lib/alma/location.rb +29 -0
- data/lib/alma/location_set.rb +21 -0
- data/lib/alma/renewal_response.rb +19 -11
- data/lib/alma/request.rb +167 -0
- data/lib/alma/request_options.rb +36 -17
- data/lib/alma/request_set.rb +64 -15
- data/lib/alma/response.rb +45 -0
- data/lib/alma/result_set.rb +27 -35
- data/lib/alma/user.rb +111 -92
- data/lib/alma/user_request.rb +19 -0
- data/lib/alma/user_set.rb +5 -6
- data/lib/alma/version.rb +3 -1
- data/lib/alma.rb +34 -22
- data/log/.gitignore +4 -0
- metadata +118 -10
- data/.travis.yml +0 -5
- data/lib/alma/api.rb +0 -33
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            ## Alma::Electronic
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            A wrapper for the Alma::Electronic API.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            The main entry point is the get methods.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ### To get a list of all the collections.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ```
         | 
| 10 | 
            +
            Alma::Electronic.get()
         | 
| 11 | 
            +
            ```
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            Will also accept these params:
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            | Parameter | Type      | Required  | Description |
         | 
| 16 | 
            +
            | --------- | ----------| --------- | ---------------------------------------------------------------------------------------------------------------|
         | 
| 17 | 
            +
            | q         | xs:string | Optional. | Search query. Optional. Searching for words in interface_name, keywords, name or po_line_id (see Brief Search) |
         | 
| 18 | 
            +
            | limit     | xs:int    | Optional. | Default: 10Limits the number of results. Optional. Valid values are 0-100. Default value: 10. |
         | 
| 19 | 
            +
            | offset    | xs:int    | Optional. | Default: 0Offset of the results returned. Optional. Default value: 0, which methodseans that the first results will be returned. |
         | 
| 20 | 
            +
             | 
| @@ -0,0 +1,224 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require "alma/electronic"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # Contains batch processing utils for Alma Electronic APIs.
         | 
| 7 | 
            +
            #
         | 
| 8 | 
            +
            # This class and its methods are used to iterate over Alma Electronic IDs to
         | 
| 9 | 
            +
            # process and fetch Alma electronic objects via the Alma Electronic APIs.  The
         | 
| 10 | 
            +
            # https calls are logged and can be used to rerun the batch process without
         | 
| 11 | 
            +
            # making any further http calls or to just rerun parts of the full batch.
         | 
| 12 | 
            +
            module Alma
         | 
| 13 | 
            +
              class Electronic::BatchUtils
         | 
| 14 | 
            +
                attr_reader :notes, :type
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # @param [Hash] options The options to create a batch instance.
         | 
| 17 | 
            +
                # @option [true, false] :chain true indicates a new instance of self returned.
         | 
| 18 | 
            +
                # @option [Array<String>] :ids List of collection ids.
         | 
| 19 | 
            +
                # @option ["collection", "service", "portfolio"] :type The Alma Electronic object type.
         | 
| 20 | 
            +
                # @option [String] :tag  A string used to tag the batch session.
         | 
| 21 | 
            +
                # @option [Logger] :logger A logger used t
         | 
| 22 | 
            +
                def initialize(options = {})
         | 
| 23 | 
            +
                  options ||= options {}
         | 
| 24 | 
            +
                  @chain = options.fetch(:chain, false)
         | 
| 25 | 
            +
                  @ids = options.fetch(:ids, [])
         | 
| 26 | 
            +
                  @type = options.fetch(:type, "collection")
         | 
| 27 | 
            +
                  @tag = options.fetch(:tag, Time.now.to_s)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  @@logger = options[:logger] || Logger.new("log/electronic_batch_process.log")
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def get_collection_notes(ids = nil, options = {})
         | 
| 33 | 
            +
                  ids ||= @ids
         | 
| 34 | 
            +
                  get_notes(options.merge(ids: make_collection_ids(ids), type: "collection"))
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def get_service_notes(ids = nil, options = {})
         | 
| 38 | 
            +
                  ids ||= @ids
         | 
| 39 | 
            +
                  get_notes(options.merge(ids: get_service_ids(ids, options), type: "service"))
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def get_portfolio_notes(ids = nil, options = {})
         | 
| 43 | 
            +
                  ids ||= @ids
         | 
| 44 | 
            +
                  get_notes(options.merge(ids: get_portfolio_ids(ids, options, type: "portfolio")))
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def get_notes(options = {})
         | 
| 48 | 
            +
                  options ||= {}
         | 
| 49 | 
            +
                  chain = options.fetch(:chain, @chain)
         | 
| 50 | 
            +
                  ids = options[:ids] || (chain ? build_ids(options) : @ids)
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  type = options.fetch(:type, @type)
         | 
| 53 | 
            +
                  tag = options.fetch(:tag, @tag)
         | 
| 54 | 
            +
                  @notes = ids.inject({}) do |notes, params|
         | 
| 55 | 
            +
                    id = get_id(type, params)
         | 
| 56 | 
            +
                    start = Time.now
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    begin
         | 
| 59 | 
            +
                      item = Alma::Electronic.get(params)
         | 
| 60 | 
            +
                    rescue StandardError => e
         | 
| 61 | 
            +
                      item = { "error" => e.message }
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    data = ["error", "authentication_note", "public_note"].reduce({}) do |acc, field|
         | 
| 65 | 
            +
                      acc[field] = item[field] if item[field].present?
         | 
| 66 | 
            +
                      acc
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    unavailable = item.dig("service_temporarily_unavailable", "value")
         | 
| 70 | 
            +
                    if unavailable == "1" || unavailable == "true"
         | 
| 71 | 
            +
                      data.merge!(item.slice("service_temporarily_unavailable", "service_unavailable_date", "service_unavailable_reason"))
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    if data.present?
         | 
| 75 | 
            +
                      log(params.merge(data).merge(type: type, start: start, tag: tag))
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                      notes[id] = data unless data["error"].present?
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    notes
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  self.class.new(options.merge(
         | 
| 84 | 
            +
                                   chain: chain,
         | 
| 85 | 
            +
                                   ids: ids,
         | 
| 86 | 
            +
                                   type: type,
         | 
| 87 | 
            +
                                   tag: tag,
         | 
| 88 | 
            +
                                   notes: notes,
         | 
| 89 | 
            +
                                   logger: @@logger,
         | 
| 90 | 
            +
                  ))
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def get_service_ids(ids = @ids, options = {})
         | 
| 94 | 
            +
                  tag = options.fetch(:tag, @tag)
         | 
| 95 | 
            +
                  start = Time.now
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  make_collection_ids(ids)
         | 
| 98 | 
            +
                    .map { |id| id.merge(type: "services") }
         | 
| 99 | 
            +
                    .inject([]) do |service_ids, params|
         | 
| 100 | 
            +
                    params.merge!(tag: tag)
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    begin
         | 
| 103 | 
            +
                      item = Alma::Electronic.get(params)
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                      if item["errorList"]
         | 
| 106 | 
            +
                        log params.merge(item["errorList"])
         | 
| 107 | 
            +
                          .merge(start: start)
         | 
| 108 | 
            +
                      else
         | 
| 109 | 
            +
                        item["electronic_service"].each { |service|
         | 
| 110 | 
            +
                          service_id = { service_id: service["id"].to_s }
         | 
| 111 | 
            +
                          service_ids << params.slice(:collection_id)
         | 
| 112 | 
            +
                            .merge(service_id)
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                          log params.merge(service_id)
         | 
| 115 | 
            +
                            .merge(start: start)
         | 
| 116 | 
            +
                        }
         | 
| 117 | 
            +
                      end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    rescue StandardError => e
         | 
| 120 | 
            +
                      log params.merge("error" => e.message)
         | 
| 121 | 
            +
                        .merge(start: start)
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    service_ids
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                # Builds the notes object using the logs.
         | 
| 129 | 
            +
                def build_notes(options = {})
         | 
| 130 | 
            +
                  options ||= {}
         | 
| 131 | 
            +
                  type ||= options.fetch(:type, "collection")
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  get_logged_items(options)
         | 
| 134 | 
            +
                    .select { |item| item.slice("authentication_note", "public_note").values.any?(&:present?) }
         | 
| 135 | 
            +
                    .inject({}) do |nodes, item|
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                    id = item["#{type}_id"]
         | 
| 138 | 
            +
                    nodes.merge(id => item.slice("authentication_note", "public_note"))
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                # Builds list of ids from logs based on failed attempts.
         | 
| 143 | 
            +
                # Useful for rebuilding part of collection.
         | 
| 144 | 
            +
                def build_failed_ids(options = {})
         | 
| 145 | 
            +
                  successful_ids = build_successful_ids(options)
         | 
| 146 | 
            +
                  get_logged_items(options)
         | 
| 147 | 
            +
                    .select { |item| item.slice("authentication_note", "public_note").values.all?(&:nil?) }
         | 
| 148 | 
            +
                    .map { |item| item["collection_id"] }
         | 
| 149 | 
            +
                    .select { |id| !successful_ids.include? id }
         | 
| 150 | 
            +
                    .uniq
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                # Builds list of ids from logs based on successful attempts.
         | 
| 154 | 
            +
                # Useful for verifying that failed ids have always failed.
         | 
| 155 | 
            +
                def build_successful_ids(options = {})
         | 
| 156 | 
            +
                  get_logged_items(options)
         | 
| 157 | 
            +
                    .select { |item| item.slice("authentication_note", "public_note").values.present? }
         | 
| 158 | 
            +
                    .map { |item| item["collection_id"] }
         | 
| 159 | 
            +
                    .uniq
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                # Builds a list of all ids for a specific session.
         | 
| 163 | 
            +
                # Useful for analytics purpose or rebuilds.
         | 
| 164 | 
            +
                def build_ids(options = {})
         | 
| 165 | 
            +
                  build_failed_ids(options) + build_successful_ids(options)
         | 
| 166 | 
            +
                end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                def print_notes(options = {})
         | 
| 169 | 
            +
                  options ||= {}
         | 
| 170 | 
            +
                  chain = options.fetch(:chain, @chain)
         | 
| 171 | 
            +
                  notes = options[:notes] || chain ? build_notes(options) : @notes
         | 
| 172 | 
            +
                  type = options.fetch(:type, @type)
         | 
| 173 | 
            +
                  tag = options.fetch(:tag, @tag)
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  filename = options.fetch(:filename, "spec/fixtures/#{type}_notes.json")
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  File.open(filename, "w") do |file|
         | 
| 178 | 
            +
                    file.write(JSON.pretty_generate(notes))
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  self.class.new(options.merge(
         | 
| 182 | 
            +
                                   chain: chain,
         | 
| 183 | 
            +
                                   notes: notes,
         | 
| 184 | 
            +
                                   type: type,
         | 
| 185 | 
            +
                                   tag: tag,
         | 
| 186 | 
            +
                                   logger: @@logger,
         | 
| 187 | 
            +
                  ))
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
              private
         | 
| 191 | 
            +
                def log(params)
         | 
| 192 | 
            +
                  if defined?(LogUtils)
         | 
| 193 | 
            +
                    LogUtils.json_request_logger(@@logger, params)
         | 
| 194 | 
            +
                  else
         | 
| 195 | 
            +
                    @@logger.info(params)
         | 
| 196 | 
            +
                  end
         | 
| 197 | 
            +
                end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                def get_id(type, params = {})
         | 
| 200 | 
            +
                  id = "#{type}_id".to_sym
         | 
| 201 | 
            +
                  params[id]
         | 
| 202 | 
            +
                end
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                def make_collection_ids(ids = @ids)
         | 
| 205 | 
            +
                  ids.map { |id|
         | 
| 206 | 
            +
                    id.class == Hash ? id : { collection_id: id.to_s }
         | 
| 207 | 
            +
                  }
         | 
| 208 | 
            +
                end
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                # Returns JSON parsed list of logged items
         | 
| 211 | 
            +
                def get_logged_items(options = {})
         | 
| 212 | 
            +
                  options ||= {}
         | 
| 213 | 
            +
                  type ||= options.fetch(:type, "collection")
         | 
| 214 | 
            +
                  tag ||= options.fetch(:tag, @tag)
         | 
| 215 | 
            +
                  filename = (@@logger.instance_variable_get :@logdev).filename
         | 
| 216 | 
            +
                  File.readlines(filename)
         | 
| 217 | 
            +
                    .map { |log| log.match(/{.*}/).to_s }
         | 
| 218 | 
            +
                    .select(&:present?)
         | 
| 219 | 
            +
                    .map { |json| JSON.parse(json) }
         | 
| 220 | 
            +
                    .select { |item| item["tag"] == tag }
         | 
| 221 | 
            +
                    .select { |item| item["type"] == type }
         | 
| 222 | 
            +
                end
         | 
| 223 | 
            +
              end
         | 
| 224 | 
            +
            end
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require "alma/electronic"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Alma
         | 
| 7 | 
            +
              # Holds some custom business logic for our Alma Electronic API.
         | 
| 8 | 
            +
              # This class is not intended for public use.
         | 
| 9 | 
            +
              class Electronic::Business
         | 
| 10 | 
            +
                # The Service ID is usually the Collection ID grouped by
         | 
| 11 | 
            +
                # 2 digits with the first number incremented by 1 and the
         | 
| 12 | 
            +
                # fifth number decremented by 1.
         | 
| 13 | 
            +
                #
         | 
| 14 | 
            +
                # @note However, this pattern does not hold for all cases.
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # @param collection_id [String] The electronic collection id.
         | 
| 17 | 
            +
                def service_id(collection_id)
         | 
| 18 | 
            +
                  collection_id.scan(/.{1,2}/).each_with_index.map { |char, index|
         | 
| 19 | 
            +
                    if index == 0
         | 
| 20 | 
            +
                      "%02d" % (char.to_i + 1)
         | 
| 21 | 
            +
                    elsif index == 4
         | 
| 22 | 
            +
                      "%02d" % (char.to_i - 1)
         | 
| 23 | 
            +
                    else
         | 
| 24 | 
            +
                      char
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  }.join
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         | 
| @@ -0,0 +1,167 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "httparty"
         | 
| 4 | 
            +
            require "active_support"
         | 
| 5 | 
            +
            require "active_support/core_ext"
         | 
| 6 | 
            +
            require "alma/config"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Alma
         | 
| 9 | 
            +
              # Alma::Electronic APIs wrapper.
         | 
| 10 | 
            +
              class Electronic
         | 
| 11 | 
            +
                class ElectronicError < ArgumentError
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def self.get(params = {})
         | 
| 15 | 
            +
                  retries_count = 0
         | 
| 16 | 
            +
                  response = nil
         | 
| 17 | 
            +
                  while retries_count < http_retries do
         | 
| 18 | 
            +
                    begin
         | 
| 19 | 
            +
                      response = get_api(params)
         | 
| 20 | 
            +
                      break;
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    rescue Net::ReadTimeout
         | 
| 23 | 
            +
                      retries_count += 1
         | 
| 24 | 
            +
                      log.error("Retrying http after timeout with : #{params}")
         | 
| 25 | 
            +
                      no_more_retries_left = retries_count == http_retries
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      raise Net::ReadTimeout.new("Failed due to net timeout after #{http_retries}: #{params}") if no_more_retries_left
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  return response
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def self.get_totals
         | 
| 35 | 
            +
                  @totals ||= get(limit: "0").data["total_record_count"]
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def self.log
         | 
| 39 | 
            +
                  Alma.configuration.logger
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def self.get_ids
         | 
| 43 | 
            +
                  total = get_totals()
         | 
| 44 | 
            +
                  limit = 100
         | 
| 45 | 
            +
                  offset = 0
         | 
| 46 | 
            +
                  log.info("Retrieving #{total} collection ids.")
         | 
| 47 | 
            +
                  groups = Array.new(total / limit + 1, limit)
         | 
| 48 | 
            +
                  @ids ||= groups.map { |limit|
         | 
| 49 | 
            +
                    prev_offset = offset
         | 
| 50 | 
            +
                    offset += limit
         | 
| 51 | 
            +
                    { offset: prev_offset, limit: limit }
         | 
| 52 | 
            +
                  }
         | 
| 53 | 
            +
                    .map { |params|  Thread.new { self.get(params) } }
         | 
| 54 | 
            +
                    .map(&:value).map(&:data)
         | 
| 55 | 
            +
                    .map { |data| data["electronic_collection"].map { |coll| coll["id"] } }
         | 
| 56 | 
            +
                    .flatten.uniq
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def self.http_retries
         | 
| 60 | 
            +
                  Alma.configuration.http_retries
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              private
         | 
| 64 | 
            +
                class ElectronicAPI
         | 
| 65 | 
            +
                  include ::HTTParty
         | 
| 66 | 
            +
                  include ::Enumerable
         | 
| 67 | 
            +
                  extend ::Forwardable
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  REQUIRED_PARAMS = []
         | 
| 70 | 
            +
                  RESOURCE = "/almaws/v1/electronic"
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  attr_reader :params, :data
         | 
| 73 | 
            +
                  def_delegators :@data, :each, :each_pair, :fetch, :values, :keys, :dig,
         | 
| 74 | 
            +
                    :slice, :except, :to_h, :to_hash, :[], :with_indifferent_access
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def initialize(params = {})
         | 
| 77 | 
            +
                    @params = params
         | 
| 78 | 
            +
                    headers = self.class::headers
         | 
| 79 | 
            +
                    log.info(url: url, query: params)
         | 
| 80 | 
            +
                    response = self.class::get(url, headers: headers, query: params, timeout: timeout)
         | 
| 81 | 
            +
                    @data = JSON.parse(response.body) rescue {}
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def url
         | 
| 85 | 
            +
                    "#{Alma.configuration.region}#{resource}"
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def timeout
         | 
| 89 | 
            +
                    Alma.configuration.timeout
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  def log
         | 
| 93 | 
            +
                    Alma::Electronic.log
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  def resource
         | 
| 97 | 
            +
                    @params.inject(self.class::RESOURCE) { |path, param|
         | 
| 98 | 
            +
                      key = param.first
         | 
| 99 | 
            +
                      value = param.last
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                      if key && value
         | 
| 102 | 
            +
                        path.gsub(/:#{key}/, value.to_s)
         | 
| 103 | 
            +
                      else
         | 
| 104 | 
            +
                        path
         | 
| 105 | 
            +
                      end
         | 
| 106 | 
            +
                    }
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  def self.can_process?(params = {})
         | 
| 110 | 
            +
                    type = self.to_s.split("::").last.parameterize
         | 
| 111 | 
            +
                    self::REQUIRED_PARAMS.all? { |param| params.include? param } &&
         | 
| 112 | 
            +
                      params[:type].blank? || params[:type] == type
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                private
         | 
| 116 | 
            +
                  def self.headers
         | 
| 117 | 
            +
                    { "Authorization": "apikey #{apikey}",
         | 
| 118 | 
            +
                     "Accept": "application/json",
         | 
| 119 | 
            +
                     "Content-Type": "application/json" }
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  def self.apikey
         | 
| 123 | 
            +
                    Alma.configuration.apikey
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                class Portfolio < ElectronicAPI
         | 
| 128 | 
            +
                  REQUIRED_PARAMS = [ :collection_id, :service_id, :portfolio_id ]
         | 
| 129 | 
            +
                  RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id/e-services/:service_id/portfolios/:portfolio_id"
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                class Service < ElectronicAPI
         | 
| 133 | 
            +
                  REQUIRED_PARAMS = [ :collection_id, :service_id ]
         | 
| 134 | 
            +
                  RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id/e-services/:service_id"
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                class Services < ElectronicAPI
         | 
| 138 | 
            +
                  REQUIRED_PARAMS = [ :collection_id, :type ]
         | 
| 139 | 
            +
                  RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id/e-services"
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                class Collection < ElectronicAPI
         | 
| 143 | 
            +
                  REQUIRED_PARAMS = [ :collection_id ]
         | 
| 144 | 
            +
                  RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id"
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                # Catch all Electronic API.
         | 
| 148 | 
            +
                # By default returns all collections
         | 
| 149 | 
            +
                class Collections < ElectronicAPI
         | 
| 150 | 
            +
                  REQUIRED_PARAMS = []
         | 
| 151 | 
            +
                  RESOURCE = "/almaws/v1/electronic/e-collections"
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                  def self.can_process?(params = {})
         | 
| 154 | 
            +
                    true
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                # Order matters because parameters can repeat.
         | 
| 159 | 
            +
                REGISTERED_APIs = [Portfolio, Service, Services, Collection, Collections]
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                def self.get_api(params)
         | 
| 162 | 
            +
                  REGISTERED_APIs
         | 
| 163 | 
            +
                    .find { |m| m.can_process? params }
         | 
| 164 | 
            +
                    .new(params)
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
              end
         | 
| 167 | 
            +
            end
         | 
    
        data/lib/alma/error.rb
    CHANGED
    
    | @@ -1,15 +1,27 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            module Alma::Error
         | 
| 3 4 | 
             
              def has_error?
         | 
| 4 5 | 
             
                !error.empty?
         | 
| 5 6 | 
             
              end
         | 
| 6 7 |  | 
| 7 8 | 
             
              def error_message
         | 
| 8 | 
            -
                (has_error?) ? error[ | 
| 9 | 
            +
                (has_error?) ? error["errorList"]["error"]["errorMessage"] : ""
         | 
| 9 10 | 
             
              end
         | 
| 10 11 |  | 
| 11 12 | 
             
              def error
         | 
| 12 | 
            -
                @response.fetch( | 
| 13 | 
            +
                @response.fetch("web_service_result", {})
         | 
| 13 14 | 
             
              end
         | 
| 15 | 
            +
            end
         | 
| 14 16 |  | 
| 15 | 
            -
             | 
| 17 | 
            +
            module Alma
         | 
| 18 | 
            +
              class StandardError < ::StandardError
         | 
| 19 | 
            +
                def initialize(message, loggable = {})
         | 
| 20 | 
            +
                  if Alma.configuration.enable_loggable
         | 
| 21 | 
            +
                    message = { error: message }.merge(loggable).to_json
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  super message
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
    
        data/lib/alma/fine.rb
    ADDED
    
    | @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Alma
         | 
| 4 | 
            +
              class Fine < AlmaRecord
         | 
| 5 | 
            +
                extend Alma::ApiDefaults
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.where_user(user_id, args = {})
         | 
| 8 | 
            +
                  response = HTTParty.get("#{users_base_path}/#{user_id}/fees", query: args, headers: headers, timeout: timeout)
         | 
| 9 | 
            +
                  if response.code == 200
         | 
| 10 | 
            +
                    Alma::FineSet.new(response)
         | 
| 11 | 
            +
                  else
         | 
| 12 | 
            +
                    raise StandardError, get_body_from(response)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
    
        data/lib/alma/fine_set.rb
    CHANGED
    
    | @@ -1,39 +1,54 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Alma
         | 
| 2 | 
            -
              class FineSet
         | 
| 3 | 
            -
                 | 
| 4 | 
            -
                 | 
| 5 | 
            -
                #include Alma::Error
         | 
| 4 | 
            +
              class FineSet < ResultSet
         | 
| 5 | 
            +
                class ResponseError < Alma::StandardError
         | 
| 6 | 
            +
                end
         | 
| 6 7 |  | 
| 7 | 
            -
                attr_reader : | 
| 8 | 
            -
                def_delegators : | 
| 9 | 
            -
                def_delegators :response, :[], :fetch
         | 
| 8 | 
            +
                attr_reader :results, :raw_response
         | 
| 9 | 
            +
                def_delegators :results, :empty?
         | 
| 10 10 |  | 
| 11 | 
            -
                def initialize( | 
| 12 | 
            -
                  @ | 
| 11 | 
            +
                def initialize(raw_response)
         | 
| 12 | 
            +
                  @raw_response = raw_response
         | 
| 13 | 
            +
                  @response = raw_response.parsed_response
         | 
| 14 | 
            +
                  validate(raw_response)
         | 
| 15 | 
            +
                  @results = @response.fetch(key, [])
         | 
| 16 | 
            +
                    .map { |item| single_record_class.new(item) }
         | 
| 13 17 | 
             
                end
         | 
| 14 18 |  | 
| 15 | 
            -
                def  | 
| 16 | 
            -
                   | 
| 19 | 
            +
                def loggable
         | 
| 20 | 
            +
                  { uri: @raw_response&.request&.uri.to_s
         | 
| 21 | 
            +
                  }.select { |k, v| !(v.nil? || v.empty?) }
         | 
| 17 22 | 
             
                end
         | 
| 18 23 |  | 
| 19 | 
            -
                def  | 
| 20 | 
            -
                   | 
| 24 | 
            +
                def validate(response)
         | 
| 25 | 
            +
                  if response.code != 200
         | 
| 26 | 
            +
                    message = "Could not find fines."
         | 
| 27 | 
            +
                    log = loggable.merge(response.parsed_response)
         | 
| 28 | 
            +
                    raise ResponseError.new(message, log)
         | 
| 29 | 
            +
                  end
         | 
| 21 30 | 
             
                end
         | 
| 22 31 |  | 
| 23 | 
            -
                def  | 
| 24 | 
            -
                   | 
| 32 | 
            +
                def each(&block)
         | 
| 33 | 
            +
                  @results.each(&block)
         | 
| 25 34 | 
             
                end
         | 
| 26 | 
            -
                alias :total_sum :sum
         | 
| 27 35 |  | 
| 28 | 
            -
                def  | 
| 29 | 
            -
                   | 
| 36 | 
            +
                def success?
         | 
| 37 | 
            +
                  raw_response.response.code.to_s == "200"
         | 
| 30 38 | 
             
                end
         | 
| 31 39 |  | 
| 32 | 
            -
                def  | 
| 33 | 
            -
                   | 
| 40 | 
            +
                def key
         | 
| 41 | 
            +
                  "fee"
         | 
| 34 42 | 
             
                end
         | 
| 35 | 
            -
                alias :total_records :total_record_count
         | 
| 36 43 |  | 
| 44 | 
            +
                def sum
         | 
| 45 | 
            +
                  fetch("total_sum", 0)
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                alias :total_sum :sum
         | 
| 37 49 |  | 
| 50 | 
            +
                def currency
         | 
| 51 | 
            +
                  fetch("currency", nil)
         | 
| 52 | 
            +
                end
         | 
| 38 53 | 
             
              end
         | 
| 39 54 | 
             
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Alma
         | 
| 4 | 
            +
              class ItemRequestOptions < RequestOptions
         | 
| 5 | 
            +
                class ResponseError < Alma::StandardError
         | 
| 6 | 
            +
                end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def self.get(mms_id, holding_id = nil, item_pid = nil, options = {})
         | 
| 9 | 
            +
                  url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items/#{item_pid}/request-options"
         | 
| 10 | 
            +
                  options.select! { |k, _|  REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
         | 
| 11 | 
            +
                  response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
         | 
| 12 | 
            +
                  new(response)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def validate(response)
         | 
| 16 | 
            +
                  if response.code != 200
         | 
| 17 | 
            +
                    message = "Could not get item request options."
         | 
| 18 | 
            +
                    log = loggable.merge(response.parsed_response)
         | 
| 19 | 
            +
                    raise ResponseError.new(message, log)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
    
        data/lib/alma/library.rb
    ADDED
    
    | @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Alma
         | 
| 4 | 
            +
              class Library < AlmaRecord
         | 
| 5 | 
            +
                extend Alma::ApiDefaults
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.all(args: {})
         | 
| 8 | 
            +
                  response = HTTParty.get("#{configuration_base_path}/libraries", query: args, headers: headers, timeout: timeout)
         | 
| 9 | 
            +
                  if response.code == 200
         | 
| 10 | 
            +
                    LibrarySet.new(response)
         | 
| 11 | 
            +
                  else
         | 
| 12 | 
            +
                    raise StandardError, get_body_from(response)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def self.find(library_code:, args: {})
         | 
| 17 | 
            +
                  response = HTTParty.get("#{configuration_base_path}/libraries/#{library_code}", query: args, headers: headers, timeout: timeout)
         | 
| 18 | 
            +
                  if response.code == 200
         | 
| 19 | 
            +
                    AlmaRecord.new(response)
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    raise StandardError, get_body_from(response)
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def self.get_body_from(response)
         | 
| 26 | 
            +
                  JSON.parse(response.body)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Alma
         | 
| 4 | 
            +
              class LibrarySet < ResultSet
         | 
| 5 | 
            +
                def_delegators :results, :[], :empty?
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def each(&block)
         | 
| 8 | 
            +
                  results.each(&block)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def results
         | 
| 12 | 
            +
                  @results ||= @response.fetch(key, [])
         | 
| 13 | 
            +
                     .map { |item| single_record_class.new(item) }
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                protected
         | 
| 17 | 
            +
                  def key
         | 
| 18 | 
            +
                    "library"
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
    
        data/lib/alma/loan.rb
    CHANGED
    
    | @@ -1,9 +1,38 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Alma
         | 
| 2 4 | 
             
              class Loan < AlmaRecord
         | 
| 5 | 
            +
                extend Alma::ApiDefaults
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
                def renewable?
         | 
| 9 | 
            +
                  !!renewable
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def renewable
         | 
| 13 | 
            +
                  response.fetch("renewable", false)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def overdue?
         | 
| 17 | 
            +
                  loan_status == "Overdue"
         | 
| 18 | 
            +
                end
         | 
| 3 19 |  | 
| 4 20 | 
             
                def renew
         | 
| 5 | 
            -
                  Alma::User.renew_loan({user_id: user_id, loan_id: loan_id})
         | 
| 21 | 
            +
                  Alma::User.renew_loan({ user_id: user_id, loan_id: loan_id })
         | 
| 6 22 | 
             
                end
         | 
| 7 23 |  | 
| 24 | 
            +
                def self.where_user(user_id, args = {})
         | 
| 25 | 
            +
                  # Always expand renewable unless you really don't want to
         | 
| 26 | 
            +
                  args[:expand] ||= "renewable"
         | 
| 27 | 
            +
                  # Default to upper limit
         | 
| 28 | 
            +
                  args[:limit] ||= 100
         | 
| 29 | 
            +
                  response = HTTParty.get(
         | 
| 30 | 
            +
                    "#{users_base_path}/#{user_id}/loans",
         | 
| 31 | 
            +
                    query: args,
         | 
| 32 | 
            +
                    headers: headers,
         | 
| 33 | 
            +
                    timeout: timeout
         | 
| 34 | 
            +
                    )
         | 
| 35 | 
            +
                  Alma::LoanSet.new(response, args)
         | 
| 36 | 
            +
                end
         | 
| 8 37 | 
             
              end
         | 
| 9 | 
            -
            end
         | 
| 38 | 
            +
            end
         |