From f5d465a0ddaf2508304a05e29cf6a0897c60eb51 Mon Sep 17 00:00:00 2001
From: Alexander Olofsson <alexander.olofsson@liu.se>
Date: Tue, 18 Oct 2022 10:52:56 +0200
Subject: [PATCH] Improve documentation and CI

---
 .gitlab-ci.yml                          |  8 +++---
 .rubocop.yml                            |  6 ++--
 README.md                               | 30 +++++++++----------
 lib/passwordstate/errors.rb             |  3 +-
 lib/passwordstate/resource.rb           | 38 +++++++++++++++++++++----
 lib/passwordstate/resources/document.rb |  3 +-
 lib/passwordstate/util.rb               | 18 ++++++++++++
 7 files changed, 77 insertions(+), 29 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c217267..1f38042 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,7 +15,7 @@ rubocop:
     - bundle exec rubocop
 
 pages:
-  stage: deploy
+  before_script: []
   script:
     - gem install yard
     - yard doc -o public/
@@ -25,6 +25,6 @@ pages:
   only:
     - master
 
-# rspec:
-#   script:
-#   - rspec spec
+rake:
+  script:
+    - bundle exec rake
diff --git a/.rubocop.yml b/.rubocop.yml
index aea3ae7..dec223c 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -35,12 +35,12 @@ Metrics/CyclomaticComplexity:
 Style/RescueModifier:
   Enabled: false
 
+Layout/LineLength:
+  Max: 140
+
 Metrics/MethodLength:
   Max: 40
 
-Metrics/LineLength:
-  Max: 190
-
 Metrics/AbcSize:
   Enabled: false
 
diff --git a/README.md b/README.md
index c981a53..b45f479 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 A ruby gem for communicating with a Passwordstate instance
 
-The documentation for the development version can be found at http://iti.gitlab-pages.liu.se/ruby-passwordstate
+The documentation for the development version can be found at https://iti.gitlab-pages.liu.se/ruby-passwordstate
 
 ## Installation
 
@@ -22,20 +22,20 @@ Or install it yourself as:
 
 ## Usage example
 
-```irb
-irb(main):001:0> require 'passwordstate'
-irb(main):002:0> client = Passwordstate::Client.new 'https://passwordstate', username: 'user', password: 'password'
-irb(main):003:0> #        Passwordstate::Client.new 'https://passwordstate', apikey: 'key'
-irb(main):004:0> client.folders
-=> [#<Passwordstate::Resources::Folder:0x000055ed493636e8 @folder_name="Example", @folder_id=2, @tree_path="\\Example">, #<Passwordstate::Resources::Folder:0x000055ed49361fa0 @folder_name="Folder", @folder_id=3, @tree_path="\\Example\\Folder">]
-irb(main):005:0> client.password_lists.get(7).passwords
-=> [#<Passwordstate::Resources::Password:0x0000555fda8acdb8 @title="Webserver1", @user_name="test_web_account", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=2>, #<Passwordstate::Resources::Password:0x0000555fda868640 @title="Webserver2", @user_name="test_web_account2", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=3>, #<Passwordstate::Resources::Password:0x0000555fda84da48 @title="Webserver3", @user_name="test_web_account3", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=4>]
-irb(main):006:0> pw = client.password_lists.first.passwords.create title: 'example', user_name: 'someone', generate_password: true
-=> #<Passwordstate::Resources::Password:0x0000555fdaf9ce98 @title="example", @user_name="someone", @account_type_id=0, @password="[ REDACTED ]", @allow_export=true, @password_id=12, @generate_password=true, @password_list_id=6>
-irb(main):007:0> pw.password
-=> "millionfE2rMrcb2LngBTHnDyxdpsGSmK3"
-irb(main):008:0> pw.delete
-=> true
+```ruby
+require 'passwordstate'
+client = Passwordstate::Client.new 'https://passwordstate', username: 'user', password: 'password'
+#        Passwordstate::Client.new 'https://passwordstate', apikey: 'key'
+client.folders
+# [#<Passwordstate::Resources::Folder:0x000055ed493636e8 @folder_name="Example", @folder_id=2, @tree_path="\\Example">, #<Passwordstate::Resources::Folder:0x000055ed49361fa0 @folder_name="Folder", @folder_id=3, @tree_path="\\Example\\Folder">]
+client.password_lists.get(7).passwords
+# [#<Passwordstate::Resources::Password:0x0000555fda8acdb8 @title="Webserver1", @user_name="test_web_account", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=2>, #<Passwordstate::Resources::Password:0x0000555fda868640 @title="Webserver2", @user_name="test_web_account2", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=3>, #<Passwordstate::Resources::Password:0x0000555fda84da48 @title="Webserver3", @user_name="test_web_account3", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=4>]
+pw = client.password_lists.first.passwords.create title: 'example', user_name: 'someone', generate_password: true
+# #<Passwordstate::Resources::Password:0x0000555fdaf9ce98 @title="example", @user_name="someone", @account_type_id=0, @password="[ REDACTED ]", @allow_export=true, @password_id=12, @generate_password=true, @password_list_id=6>
+pw.password
+# "millionfE2rMrcb2LngBTHnDyxdpsGSmK3"
+pw.delete
+# true
 ```
 
 ## Contributing
diff --git a/lib/passwordstate/errors.rb b/lib/passwordstate/errors.rb
index b8719f4..2f4fd02 100644
--- a/lib/passwordstate/errors.rb
+++ b/lib/passwordstate/errors.rb
@@ -14,7 +14,8 @@ module Passwordstate
       @response = response
       @errors = errors
 
-      super "Passwordstate responded with an error to the request:\n#{errors.map { |err| err['message'] || err['phrase'] || err['error'] }.join('; ')}"
+      errorstr = errors.map { |err| err['message'] || err['phrase'] || err['error'] }.join('; ')
+      super "Passwordstate responded with an error to the request:\n#{errorstr}"
     end
 
     def self.new_by_code(code, req, res, errors = [])
diff --git a/lib/passwordstate/resource.rb b/lib/passwordstate/resource.rb
index 939a9a4..4601c2b 100644
--- a/lib/passwordstate/resource.rb
+++ b/lib/passwordstate/resource.rb
@@ -1,24 +1,33 @@
 # frozen_string_literal: true
 
 module Passwordstate
-  # A simple resource DSL
+  # A simple resource DSL helper
+  #
   # rubocop:disable Metrics/ClassLength This DSL class will be large
   class Resource
     attr_reader :client
 
+    # Update the object based off of up-to-date upstream data
+    # @return [Resource] self
     def get(**query)
       set! self.class.get(client, send(self.class.index_field), **query)
     end
 
+    # Push any unapplied changes to the object
+    # @return [Resource] self
     def put(body = {}, **query)
       to_send = modified.merge(self.class.index_field => send(self.class.index_field))
       set! self.class.put(client, to_send.merge(body), **query).first
     end
 
+    # Create the object based off of provided information
+    # @return [Resource] self
     def post(body = {}, **query)
       set! self.class.post(client, attributes.merge(body), **query)
     end
 
+    # Delete the object
+    # @return [Resource] self
     def delete(**query)
       self.class.delete(client, send(self.class.index_field), **query)
     end
@@ -29,14 +38,17 @@ module Passwordstate
       old
     end
 
+    # Is the object stored on the Passwordstate server
     def stored?
       !send(self.class.index_field).nil?
     end
 
+    # Check if the resource type is available on the connected Passwordstate server
     def self.available?(_client)
       true
     end
 
+    # Retrieve all accessible instances of the resource type, requires the method :get
     def self.all(client, **query)
       raise NotAcceptableError, "Read is not implemented for #{self}" unless acceptable_methods.include? :get
 
@@ -49,6 +61,7 @@ module Passwordstate
       end
     end
 
+    # Retrieve a specific instance of the resource type, requires the method :get
     def self.get(client, object, **query)
       raise NotAcceptableError, "Read is not implemented for #{self}" unless acceptable_methods.include? :get
 
@@ -68,6 +81,7 @@ module Passwordstate
       resp
     end
 
+    # Create a new instance of the resource type, requires the method :post
     def self.post(client, data, **query)
       raise NotAcceptableError, "Create is not implemented for #{self}" unless acceptable_methods.include? :post
 
@@ -79,6 +93,7 @@ module Passwordstate
       new [client.request(:post, path, body: data, query: query, reason: reason)].flatten.first.merge(_client: client)
     end
 
+    # Push new data to an instance of the resource type, requires the method :put
     def self.put(client, data, **query)
       raise NotAcceptableError, "Update is not implemented for #{self}" unless acceptable_methods.include? :put
 
@@ -90,6 +105,7 @@ module Passwordstate
       client.request :put, path, body: data, query: query, reason: reason
     end
 
+    # Delete an instance of the resource type, requires the method :delete
     def self.delete(client, object, **query)
       raise NotAcceptableError, "Delete is not implemented for #{self}" unless acceptable_methods.include? :delete
 
@@ -101,10 +117,11 @@ module Passwordstate
       client.request :delete, "#{path}/#{object}", query: query, reason: reason
     end
 
-    def api_path
-      self.class.instance_variable_get :@api_path
-    end
-
+    # Get a hash of all active attributes on the resource instance
+    #
+    # @param ignore_redact [Boolean] Should any normally redacted fields be displayed in clear-text
+    # @param atify [Boolean] Should the resulting hash be returned with instance variable names ('@'-prefixed)
+    # @option opts nil_as_string [Boolean] (resource dependent) Should nil values be treated as the empty string
     def attributes(ignore_redact: true, atify: false, **opts)
       nil_as_string = opts.fetch(:nil_as_string, self.class.nil_as_string)
       (self.class.send(:accessor_field_names) + self.class.send(:read_field_names) + self.class.send(:write_field_names)).to_h do |field|
@@ -136,6 +153,10 @@ module Passwordstate
 
     protected
 
+    def api_path
+      self.class.instance_variable_get :@api_path
+    end
+
     def modified
       attribs = attributes
       attribs.reject { |field| old[field] == attribs[field] }
@@ -175,21 +196,25 @@ module Passwordstate
     class << self
       alias search all
 
+      # Get/Set the API path for the resource type
       def api_path(path = nil)
         @api_path = path unless path.nil?
         @api_path
       end
 
+      # Get/Set the index field for the resource type
       def index_field(field = nil)
         @index_field = field unless field.nil?
         @index_field
       end
 
+      # Get/Set whether the resource type requires nil values to be handled as the empty string
       def nil_as_string(opt = nil)
         @nil_as_string = opt unless opt.nil?
         @nil_as_string
       end
 
+      # Get/Set acceptable CRUD methods for the resource type
       def acceptable_methods(*meths)
         if meths.empty?
           @acceptable_methods || %i[post get put delete]
@@ -205,15 +230,18 @@ module Passwordstate
         end
       end
 
+      # Convert a hash from Ruby syntax to Passwordstate syntax
       def passwordstateify_hash(hash)
         hash.transform_keys { |k| ruby_to_passwordstate_field(k) }
       end
 
+      # Convert a Passwordstate field name into Ruby syntax
       def passwordstate_to_ruby_field(field)
         opts = send(:field_options).find { |(_k, v)| v[:name] == field }
         opts&.first || field.to_s.snake_case.to_sym
       end
 
+      # Convert a Ruby field name into Passwordstate syntax
       def ruby_to_passwordstate_field(field)
         send(:field_options)[field]&.[](:name) || field.to_s.camel_case
       end
diff --git a/lib/passwordstate/resources/document.rb b/lib/passwordstate/resources/document.rb
index 6287a63..26decca 100644
--- a/lib/passwordstate/resources/document.rb
+++ b/lib/passwordstate/resources/document.rb
@@ -40,7 +40,8 @@ module Passwordstate
         private
 
         def validate_store!(store)
-          raise ArgumentError, 'Store must be one of password, passwordlist, folder' unless %i[password passwordlist folder].include?(store.to_s.downcase.to_sym)
+          raise ArgumentError, 'Store must be one of password, passwordlist, folder' \
+            unless %i[password passwordlist folder].include?(store.to_s.downcase.to_sym)
 
           store.to_s.downcase.to_sym
         end
diff --git a/lib/passwordstate/util.rb b/lib/passwordstate/util.rb
index 649a257..8bc5d01 100644
--- a/lib/passwordstate/util.rb
+++ b/lib/passwordstate/util.rb
@@ -4,6 +4,11 @@ require 'net/http'
 require 'net/ntlm'
 
 module Net
+  # NTLM header extension
+  #
+  # @example Setting NTLM auth
+  #   req = Net::HTTP::Get.new URI('https://example.com')
+  #   req.ntlm_auth 'username', 'password'
   module HTTPHeader
     attr_reader :ntlm_auth_information, :ntlm_auth_options
 
@@ -22,10 +27,12 @@ module Net
 end
 
 class String
+  # Convert a snake_case string to CamelCase
   def camel_case
     split('_').collect(&:capitalize).join
   end
 
+  # Convert a CamelCase string to snake_case
   def snake_case
     gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
       .gsub(/([a-z\d])([A-Z])/, '\1_\2')
@@ -33,6 +40,7 @@ class String
       .downcase
   end
 
+  # Find a specific line in a block of text
   def find_line(&_block)
     raise ArgumentError, 'No block given' unless block_given?
 
@@ -43,6 +51,16 @@ class String
 end
 
 module Passwordstate
+  # Extensions on Net::HTTP to allow NTLM digest auth
+  #
+  # @example Using NTLM auth
+  #   uri = URI('https://example.com/some_object')
+  #   Net::HTTP.start uri.host, uri.port { |http|
+  #     req = Net::HTTP::Get.new uri
+  #     req.ntlm_auth 'username', 'password'
+  #
+  #     http.request req
+  #   }
   module NetHTTPExtensions
     def request(req, body = nil, &block)
       return super(req, body, &block) if req.ntlm_auth_information.nil?
-- 
GitLab