ManageIQ/azure-armrest

View on GitHub
lib/azure/armrest/model/base_model.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
99%
require 'active_support'
require 'active_support/core_ext/string/inflections'
require 'pp'

module Azure
  module Armrest
    # Base class for JSON wrapper classes. Each Service class should have
    # a corresponding class that wraps the JSON it collects, and each of
    # them should subclass this base class.
    class BaseModel
      # Initially inherit the exclusion list from parent class or create an empty Set.
      def self.excl_list
        @excl_list ||= superclass.respond_to?(:excl_list, true) ? superclass.send(:excl_list) : Set.new
      end

      private_class_method :excl_list

      # Merge the declared exclusive attributes to the existing list.
      def self.attr_hash(*attrs)
        @excl_list = excl_list | Set.new(attrs.map(&:to_s))
      end

      private_class_method :attr_hash

      attr_hash :tags

      # Defines attr_reader methods for the given set of attributes and
      # expected hash key.  Used to define methods that can be used internally
      # that avoid needing to use methods defined from
      # `add_accessor_methods`/`__setobj__`
      #
      # Example:
      #   class Vm < Azure::ArmRest::BaseModel
      #     attr_from_hash :name => :Name
      #   end
      #
      #   json_string = {'name' => 'Deathstar'}
      #
      #   vm = Vm.new(json_string)
      #   vm.name_from_hash
      #   #=> "Deathstar"
      #
      #   # If the attr_from_hash can also support multiple attrs in a single
      #   # call, and nested params
      #
      #   class Host < Azure::ArmRest::BaseModel
      #     attr_from_hash :name => :Name,
      #                    :address => [:Properties, :ipAddress],
      #   end
      #
      #   json_string = {'name' => 'Hoth', :Properties => {:ipAddress => '123.123.123.123'}}
      #
      #   host = Host.new(json_string)
      #   host.name_from_hash
      #   #=> "Hoth"
      #   host.address_from_hash
      #   #=> "123.123.123.123"
      #
      def self.attr_from_hash(attrs = {})
        location   = caller_locations(1, 1).first
        file, line = location.path, location.lineno

        attrs.each do |attr_name, keys|
          keys      = Array(keys)
          first_key = keys.shift
          method_def = [
            "def #{attr_name}_from_hash",
            "  return @#{attr_name}_from_hash if defined?(@#{attr_name}_from_hash)",
            "  @#{attr_name}_from_hash = __getobj__[:#{first_key}] || __getobj__[\"#{first_key}\"]",
            "end"
          ]
          keys.each do |hash_key|
            method_def.insert(-2, "  @#{attr_name}_from_hash = @#{attr_name}_from_hash[:#{hash_key}] || @#{attr_name}_from_hash[\"#{hash_key}\"]")
          end
          class_eval(method_def.join("; "), file, line.to_i)
        end
      end

      private_class_method :attr_from_hash

      attr_accessor :response_headers
      attr_accessor :response_code

      # Constructs and returns a new JSON wrapper class. Pass in a plain
      # JSON string and it will automatically give you accessor methods
      # that make it behave like a typical Ruby object. You may also pass
      # in a hash.
      #
      # Example:
      #   class Person < Azure::ArmRest::BaseModel; end
      #
      #   json_string = '{"firstname":"jeff", "lastname":"durand",
      #     "address": { "street":"22 charlotte rd", "zipcode":"01013"}
      #   }'
      #
      #   # Or whatever your subclass happens to be.
      #   person = Person.new(json_string)
      #
      #   # The JSON properties are now available as methods.
      #   person.firstname        # => 'jeff'
      #   person.address.zipcode  # => '01013'
      #
      #   # Or you can get back the original JSON if necessary.
      #   person.to_json # => Returns original JSON
      #
      def initialize(json, skip_accessors_definition = false)
        # Find the exclusion list for the model of next level (@embed_model)
        # '#' is the separator between levels. Remove attributes
        # before the first separator.
        @child_excl_list = self.class.send(:excl_list).map do |e|
          e.index('#') ? e[e.index('#') + 1..-1] : ''
        end

        if json.kind_of?(Hash)
          @hash = json
        else
          @hash = JSON.parse(json)
          @json = json
        end

        @hashobj = @hash.dup
        __setobj__ unless skip_accessors_definition
      end

      def resource_group
        @resource_group ||= begin
                              id_from_hash[/resourcegroups\/(.*?[^\/]+)?/i, 1]
                            rescue
                              nil
                            end
      end

      def subscription_id
        @subscription_id ||= begin
                               id_from_hash[/subscriptions\/(.*?[^\/]+)?/i, 1]
                             rescue
                               nil
                             end
      end

      attr_writer :resource_group
      attr_writer :subscription_id

      def to_h
        @hash
      end

      def to_hash
        @hash
      end

      # Return the original JSON for the model object. The +options+ argument
      # is for interface compatibility only.
      #
      def to_json(_options = nil)
        @json ||= @hash ? @hash.to_json : ""
      end

      def to_s
        @json ||= @hash ? @hash.to_json : ""
      end

      def to_str
        @json ||= @hash ? @hash.to_json : ""
      end

      def pretty_print(q)
        inspect_method_list = methods(false).reject { |m| m.to_s.end_with?('=') }

        q.object_address_group(self) {
          q.seplist(inspect_method_list, lambda { q.text ',' }) {|v|
            q.breakable
            q.text v.to_s
            q.text '='
            q.group(1) {
              q.breakable ''
              q.pp(send(v))
            }
          }
        }
      end

      alias_method :inspect, :pretty_print_inspect

      def ==(other)
        return false unless other.kind_of?(BaseModel)
        __getobj__ == other.__getobj__
      end

      def eql?(other)
        return false unless other.kind_of?(BaseModel)
        __getobj__.eql?(other.__getobj__)
      end

      # Support hash style accessors
      def [](key)
        __getobj__[key]
      end

      def []=(key, val)
        key_exists = __getobj__.include?(key)
        __getobj__[key] = val

        return if key_exists
        add_accessor_methods(key.to_s.underscore, key)
      end

      protected

      # Do not use this method directly.
      def __getobj__
        @hashobj
      end

      # Do not use this method directly.
      #
      # Will only attempt to fetch the id from the @hashobj once, so even it it
      # is nil, it will cache that value, and return that on subsequent calls.
      def id_from_hash
        return @id_from_hash if defined?(@id_from_hash)
        @id_from_hash = __getobj__[:id] || __getobj__["id"]
      end

      # Create snake_case accessor methods for all hash attributes
      # Use _alias if an accessor conflicts with existing methods
      def __setobj__
        excl_list = self.class.send(:excl_list)
        @hashobj.each do |key, value|
          snake = key.to_s.gsub(/\W/, '_').underscore

          unless excl_list.include?(snake) # Must deal with nested models
            if value.kind_of?(Array)
              newval = value.map { |elem| elem.kind_of?(Hash) ? nested_object(snake.camelize.singularize, elem) : elem }
              @hashobj[key] = newval
            elsif value.kind_of?(Hash)
              @hashobj[key] = nested_object(snake.camelize, value)
            end
          end

          add_accessor_methods(snake, key)
        end
      end

      def nested_object(klass_name, value)
        unless self.class.const_defined?(klass_name, false)
          child_excl_list = @child_excl_list
          self.class.const_set(klass_name, Class.new(BaseModel) { attr_hash(*child_excl_list) })
        end
        self.class.const_get(klass_name).new(value)
      end

      def add_accessor_methods(method, key)
        method = "_#{method}" if respond_to?(method)
        instance_eval { define_singleton_method(method) { __getobj__[key] } }
        instance_eval { define_singleton_method("#{method}=") { |val| __getobj__[key] = val } }
      end
    end

    # Initial class definitions. Reopen these classes as needed.

    class AvailabilitySet < BaseModel; end
    class Container < BaseModel; end
    class Endpoint < BaseModel; end
    class Event < BaseModel; end
    class ExtensionType < BaseModel; end
    class ImageVersion < BaseModel; end
    class Location < BaseModel; end
    class Offer < BaseModel; end
    class Publisher < BaseModel; end
    class Resource < BaseModel; end
    class ResourceGroup < BaseModel; end
    class ResourceProvider < BaseModel; end
    class Sku < BaseModel; end
    class KeyVault < BaseModel; end

    module Billing
      class Usage < BaseModel; end
    end

    class ResponseBody < BaseModel; end

    class ResponseHeaders < BaseModel
      undef_method :response_headers
    end

    class StorageAccount < BaseModel; end
    class StorageAccountKey < StorageAccount
      attr_from_hash :key_name => :keyName,
                     :value    => :value

      def key1; key_name_from_hash == 'key1' ? value_from_hash : nil; end
      def key2; key_name_from_hash == 'key2' ? value_from_hash : nil; end
      def key; key1 || key2; end
    end

    class Subscription < BaseModel; end
    class Tag < BaseModel; end
    class TemplateDeployment < BaseModel
      attr_hash 'properties#parameters', 'properties#outputs'
    end
    class TemplateDeploymentOperation < TemplateDeployment; end
    class Tenant < BaseModel; end
    class VirtualMachine < BaseModel; end
    class VirtualMachineInstance < VirtualMachine; end
    class VirtualMachineModel < VirtualMachine; end
    class VirtualMachineExtension < BaseModel; end
    class VirtualMachineImage < BaseModel; end
    class VirtualMachineSize < BaseModel; end

    module HDInsight
      class HDInsightCluster < BaseModel; end
      class HDInsightApplication < BaseModel; end
    end

    module Insights
      class Alert < BaseModel; end
      class Diagnostic < BaseModel; end
      class Event < BaseModel; end
      class Metric < BaseModel; end
      class MetricDefinition < BaseModel; end
    end

    module Network
      class LoadBalancer < BaseModel; end
      class InboundNat < LoadBalancer; end
      class IpAddress < BaseModel; end
      class NetworkInterface < BaseModel; end
      class NetworkSecurityGroup < BaseModel; end
      class NetworkSecurityRule < NetworkSecurityGroup; end
      class RouteTable < BaseModel; end
      class Route < RouteTable; end
      class VirtualNetwork < BaseModel; end
      class Subnet < VirtualNetwork; end
    end

    module Role
      class Assignment < BaseModel; end
      class Definition < BaseModel; end
    end

    module Sql
      class MariadbServer < BaseModel; end
      class MariadbDatabase < BaseModel; end
      class MysqlServer < BaseModel; end
      class MysqlDatabase < BaseModel; end
      class PostgresqlServer < BaseModel; end
      class PostgresqlDatabase < BaseModel; end
      class SqlServer < BaseModel; end
      class SqlDatabase < BaseModel; end
    end

    module Storage
      class Disk < BaseModel; end
      class Image < BaseModel; end
      class Snapshot < BaseModel; end
    end
  end
end

require_relative 'storage_account'
require_relative 'virtual_machine'