acfs 1.3.3 → 1.6.0

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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +372 -0
  3. data/LICENSE +22 -0
  4. data/README.md +321 -0
  5. data/acfs.gemspec +38 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +26 -0
  8. data/lib/acfs/adapter/typhoeus.rb +82 -0
  9. data/lib/acfs/collection.rb +28 -0
  10. data/lib/acfs/collections/paginatable.rb +76 -0
  11. data/lib/acfs/configuration.rb +120 -0
  12. data/lib/acfs/errors.rb +147 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +76 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +31 -0
  17. data/lib/acfs/middleware/logger.rb +23 -0
  18. data/lib/acfs/middleware/msgpack.rb +32 -0
  19. data/lib/acfs/middleware/print.rb +23 -0
  20. data/lib/acfs/middleware/serializer.rb +41 -0
  21. data/lib/acfs/operation.rb +96 -0
  22. data/lib/acfs/request.rb +32 -0
  23. data/lib/acfs/request/callbacks.rb +54 -0
  24. data/lib/acfs/resource.rb +39 -0
  25. data/lib/acfs/resource/attributes.rb +270 -0
  26. data/lib/acfs/resource/attributes/base.rb +29 -0
  27. data/lib/acfs/resource/attributes/boolean.rb +39 -0
  28. data/lib/acfs/resource/attributes/date_time.rb +32 -0
  29. data/lib/acfs/resource/attributes/dict.rb +39 -0
  30. data/lib/acfs/resource/attributes/float.rb +33 -0
  31. data/lib/acfs/resource/attributes/integer.rb +29 -0
  32. data/lib/acfs/resource/attributes/list.rb +36 -0
  33. data/lib/acfs/resource/attributes/string.rb +26 -0
  34. data/lib/acfs/resource/attributes/uuid.rb +48 -0
  35. data/lib/acfs/resource/dirty.rb +37 -0
  36. data/lib/acfs/resource/initialization.rb +31 -0
  37. data/lib/acfs/resource/loadable.rb +35 -0
  38. data/lib/acfs/resource/locatable.rb +135 -0
  39. data/lib/acfs/resource/operational.rb +26 -0
  40. data/lib/acfs/resource/persistence.rb +258 -0
  41. data/lib/acfs/resource/query_methods.rb +266 -0
  42. data/lib/acfs/resource/service.rb +44 -0
  43. data/lib/acfs/resource/validation.rb +49 -0
  44. data/lib/acfs/response.rb +30 -0
  45. data/lib/acfs/response/formats.rb +27 -0
  46. data/lib/acfs/response/status.rb +33 -0
  47. data/lib/acfs/rspec.rb +13 -0
  48. data/lib/acfs/runner.rb +102 -0
  49. data/lib/acfs/service.rb +94 -0
  50. data/lib/acfs/service/middleware.rb +58 -0
  51. data/lib/acfs/service/middleware/stack.rb +65 -0
  52. data/lib/acfs/singleton_resource.rb +85 -0
  53. data/lib/acfs/stub.rb +199 -0
  54. data/lib/acfs/util.rb +22 -0
  55. data/lib/acfs/version.rb +16 -0
  56. data/lib/acfs/yard.rb +6 -0
  57. data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
  58. data/spec/acfs/collection_spec.rb +157 -0
  59. data/spec/acfs/configuration_spec.rb +53 -0
  60. data/spec/acfs/global_spec.rb +140 -0
  61. data/spec/acfs/location_spec.rb +25 -0
  62. data/spec/acfs/middleware/json_spec.rb +79 -0
  63. data/spec/acfs/middleware/msgpack_spec.rb +62 -0
  64. data/spec/acfs/operation_spec.rb +12 -0
  65. data/spec/acfs/request/callbacks_spec.rb +48 -0
  66. data/spec/acfs/request_spec.rb +79 -0
  67. data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
  68. data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
  69. data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
  70. data/spec/acfs/resource/attributes/float_spec.rb +61 -0
  71. data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
  72. data/spec/acfs/resource/attributes/list_spec.rb +60 -0
  73. data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
  74. data/spec/acfs/resource/attributes_spec.rb +179 -0
  75. data/spec/acfs/resource/dirty_spec.rb +49 -0
  76. data/spec/acfs/resource/initialization_spec.rb +36 -0
  77. data/spec/acfs/resource/loadable_spec.rb +22 -0
  78. data/spec/acfs/resource/locatable_spec.rb +118 -0
  79. data/spec/acfs/resource/persistance_spec.rb +322 -0
  80. data/spec/acfs/resource/query_methods_spec.rb +548 -0
  81. data/spec/acfs/resource/validation_spec.rb +129 -0
  82. data/spec/acfs/response/formats_spec.rb +52 -0
  83. data/spec/acfs/response/status_spec.rb +71 -0
  84. data/spec/acfs/runner_spec.rb +95 -0
  85. data/spec/acfs/service/middleware_spec.rb +35 -0
  86. data/spec/acfs/service_spec.rb +48 -0
  87. data/spec/acfs/singleton_resource_spec.rb +17 -0
  88. data/spec/acfs/stub_spec.rb +345 -0
  89. data/spec/acfs_spec.rb +205 -0
  90. data/spec/fixtures/config.yml +14 -0
  91. data/spec/spec_helper.rb +42 -0
  92. data/spec/support/hash.rb +11 -0
  93. data/spec/support/response.rb +12 -0
  94. data/spec/support/service.rb +92 -0
  95. data/spec/support/shared/find_callbacks.rb +50 -0
  96. metadata +159 -26
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # DateTime attribute type. Use it in your model as
7
+ # an attribute type:
8
+ #
9
+ # @example
10
+ # class User < Acfs::Resource
11
+ # attribute :name, :date_time
12
+ # end
13
+ #
14
+ class DateTime < Base
15
+ # @api public
16
+ #
17
+ # Cast given object to DateTime.
18
+ #
19
+ # @param [Object] value Object to cast.
20
+ # @return [DateTime] Casted object as DateTime.
21
+ #
22
+ def cast_value(value)
23
+ if value.blank?
24
+ nil
25
+ elsif !value.is_a?(::String) && value.respond_to?(:to_datetime)
26
+ value.to_datetime
27
+ else
28
+ ::DateTime.iso8601 value
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # Dict attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User
10
+ # include Acfs::Model
11
+ # attribute :opts, :dict
12
+ # end
13
+ #
14
+ class Dict < Base
15
+ # @api public
16
+ #
17
+ # Cast given object to a dict/hash.
18
+ #
19
+ # @param [Object] value Object to cast.
20
+ # @return [Hash] Casted object as hash.
21
+ # @raise [TypeError] If object cannot be casted to a hash.
22
+ #
23
+ def cast_value(value)
24
+ return {} if value.blank?
25
+
26
+ if value.is_a?(Hash)
27
+ value
28
+ elsif value.respond_to?(:serializable_hash)
29
+ value.serializable_hash
30
+ elsif value.respond_to?(:to_hash)
31
+ value.to_hash
32
+ elsif value.respond_to?(:to_h)
33
+ value.to_h
34
+ else
35
+ Hash(value)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # Float attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User < Acfs::Resource
10
+ # attribute :name, :float
11
+ # end
12
+ #
13
+ class Float < Base
14
+ # @api public
15
+ #
16
+ # Cast given object to float.
17
+ #
18
+ # @param [Object] value Object to cast.
19
+ # @return [Float] Casted object as float.
20
+ #
21
+ def cast_value(value)
22
+ return 0.0 if value.blank?
23
+
24
+ case value
25
+ when ::Float then value
26
+ when 'Infinity' then ::Float::INFINITY
27
+ when '-Infinity' then -::Float::INFINITY
28
+ when 'NaN' then ::Float::NAN
29
+ else Float(value)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # Integer attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User < Acfs::Resource
10
+ # attribute :name, :integer
11
+ # end
12
+ #
13
+ class Integer < Base
14
+ # @api public
15
+ #
16
+ # Cast given object to integer.
17
+ #
18
+ # @param [Object] value Object to cast.
19
+ # @return [Fixnum] Casted object as fixnum.
20
+ #
21
+ def cast_value(value)
22
+ if value.blank?
23
+ 0
24
+ else
25
+ Integer(value)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # List attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User < Acfs::Resource
10
+ # attribute :name, :list
11
+ # end
12
+ #
13
+ class List < Base
14
+ # @api public
15
+ #
16
+ # Cast given object to a list.
17
+ #
18
+ # @param [Object] value Object to cast.
19
+ # @return [Fixnum] Casted object as list.
20
+ # @raise [TypeError] If object cannot be casted to a list.
21
+ #
22
+ def cast_value(value)
23
+ return [] if value.blank?
24
+
25
+ if value.is_a?(::Array)
26
+ value
27
+ elsif value.respond_to?(:to_ary)
28
+ value.to_ary
29
+ elsif value.respond_to?(:to_a)
30
+ value.to_a
31
+ else
32
+ Array(value)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # String attribute type. Use it in your model as
7
+ # an attribute type:
8
+ #
9
+ # @example
10
+ # class User < Acfs::Resource
11
+ # attribute :name, :string
12
+ # end
13
+ #
14
+ class String < Base
15
+ # @api public
16
+ #
17
+ # Cast given object to string.
18
+ #
19
+ # @param [Object] value Object to cast.
20
+ # @return [String] Casted string.
21
+ #
22
+ def cast_value(value)
23
+ value.to_s
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # UUID attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User < Acfs::Resource
10
+ # attribute :id, :uuid
11
+ # end
12
+ #
13
+ class UUID < Base
14
+ UUID_REGEXP = /[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/i.freeze
15
+
16
+ # @api public
17
+ #
18
+ # Check if given object looks like a UUID, eg:
19
+ # `450b7a40-94ad-11e3-baa8-0800200c9a66`
20
+ # Valid UUIDs are 16 byte numbers represented as
21
+ # a hexadecimal string in five sub-groups seperated
22
+ # by a dash. Each group has to consist of a fixed
23
+ # number of hexadecimal digits:
24
+ # | Group | Digits |
25
+ # | -----:|:------ |
26
+ # | 1 | 8 |
27
+ # | 2 | 4 |
28
+ # | 3 | 4 |
29
+ # | 4 | 4 |
30
+ # | 5 | 12 |
31
+ #
32
+ # @param [Object] value Object to cast.
33
+ # @return [String] Casted object as UUID.
34
+ #
35
+ def cast_value(value)
36
+ if value.blank?
37
+ nil
38
+ elsif value.to_s =~ UUID_REGEXP
39
+ value
40
+ else
41
+ raise TypeError.new "Invalid UUID: `#{value}'"
42
+ end
43
+ end
44
+ end
45
+
46
+ # Lower-case alias for automatic type lookup
47
+ Uuid = UUID
48
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ #
5
+ # Thin wrapper around ActiveModel::Dirty
6
+ #
7
+ module Dirty
8
+ extend ActiveSupport::Concern
9
+ include ActiveModel::Dirty
10
+
11
+ # @api private
12
+ #
13
+ def reset_changes
14
+ clear_changes_information
15
+ end
16
+
17
+ # @api private
18
+ #
19
+ def save!(**kwargs)
20
+ super(**kwargs).tap {|_| changes_applied }
21
+ end
22
+
23
+ # @api private
24
+ #
25
+ def loaded!
26
+ reset_changes
27
+ super
28
+ end
29
+
30
+ # @api private
31
+ #
32
+ def write_raw_attribute(name, value, opts = {})
33
+ attribute_will_change!(name) if opts[:change].nil? || opts[:change]
34
+ super
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ #
5
+ # Initialization drop-in for pre-4.0 ActiveModel.
6
+ #
7
+ module Initialization
8
+ #
9
+ # @api public
10
+ #
11
+ # Initializes a new model with the given `params`.
12
+ #
13
+ # @example
14
+ # class User < Acfs::Resource
15
+ # attribute :name
16
+ # attribute :email, default: ->{ "#{name}@dom.tld" }
17
+ # attribute :age, :integer, default: 18
18
+ # end
19
+ #
20
+ # user = User.new({name: 'bob'})
21
+ # user.name # => "bob"
22
+ # user.email # => "bob@dom.tld"
23
+ # user.age # => 18
24
+ #
25
+ # @param attributes [Hash{Symbol => Object}] Attributes to set on resource.
26
+ #
27
+ def initialize(attributes = {})
28
+ write_attributes(attributes) if attributes
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ # Provides method to check for loading state of resources.
5
+ # A resource that is created but not yet fetched will be loaded
6
+ # after running {Acfs::Global#run Acfs.run}.
7
+ #
8
+ # @example
9
+ # user = User.find 5
10
+ # user.loaded? # => false
11
+ # Acfs.run
12
+ # user.loaded? # => true
13
+ #
14
+ module Loadable
15
+ extend ActiveSupport::Concern
16
+
17
+ # @api public
18
+ #
19
+ # Check if model is loaded or if request is still queued.
20
+ #
21
+ # @return [Boolean] True if resource is loaded, false otherwise.
22
+ #
23
+ def loaded?
24
+ @loaded.nil? ? false : @loaded
25
+ end
26
+
27
+ # @api private
28
+ #
29
+ # Mark model as loaded.
30
+ #
31
+ def loaded!
32
+ @loaded = true
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ # Provide methods for generation URLs for resources.
5
+ #
6
+ # @example
7
+ # class User
8
+ # service AccountService # With base URL `http://acc.svr`
9
+ # end
10
+ # User.url # => "http://acc.svr/users"
11
+ # User.url(5) # => "http://acc.svr/users/5"
12
+ #
13
+ module Locatable
14
+ extend ActiveSupport::Concern
15
+
16
+ module ClassMethods
17
+ # @overload url(suffix)
18
+ # @deprecated
19
+ # Return URL for this class of resource. Given suffix
20
+ # will be appended.
21
+ #
22
+ # @example
23
+ # User.url # => "http://users.srv.org/users"
24
+ # User.url(5) # => "http://users.srv.org/users/5"
25
+ #
26
+ # @param suffix [String] Suffix to append to URL.
27
+ # @return [String] Generated URL.
28
+ #
29
+ # @overload url(opts = {})
30
+ # Return URL for this class of resources. Given options
31
+ # will be used to replace URL path arguments and to
32
+ # determine the operation action.
33
+ #
34
+ # @example
35
+ # User.url(id: 5, action: :read) # => "http://users.srv.org/users/5"
36
+ # User.url(action: :list) # => "http://users.srv.org/users"
37
+ #
38
+ # @param opts [Hash] Options.
39
+ # @option opts [Symbol] :action Operation action,
40
+ # usually `:list`, `:create`, `:read`, `:update` or`:delete`.
41
+ # @return [String] Generated URL.
42
+ #
43
+ def url(suffix = nil, **opts)
44
+ if suffix.is_a? Hash
45
+ opts = suffix
46
+ suffix = nil
47
+ end
48
+
49
+ kwargs = {}
50
+ kwargs[:path] = opts[:path] if opts.key?(:path)
51
+ kwargs[:action] = opts.delete(:action) if opts.key?(:action)
52
+ kwargs[:action] = :list if suffix
53
+
54
+ url = location(**kwargs).build(opts.stringify_keys).str
55
+ url += "/#{suffix}" if suffix.to_s.present?
56
+ url
57
+ end
58
+
59
+ # Return a location object able to build the URL for this
60
+ # resource and given action.
61
+ #
62
+ # @example
63
+ # class Identity < ::Acfs::Resource
64
+ # service MyService, path: 'users/:user_id/identities'
65
+ # end
66
+ #
67
+ # location = Identity.location(action: :read)
68
+ # location.arguments
69
+ # => [:user_id, :id]
70
+ #
71
+ # location.raw_url
72
+ # => 'http://service/users/:user_id/identities/:id'
73
+ #
74
+ # location = Identity.location(action: :list)
75
+ # location.arguments
76
+ # => [:user_id]
77
+ #
78
+ # location.build(user_id: 42)
79
+ # => 'http://service/users/42/identities'
80
+ #
81
+ # @param opts [Hash] Options.
82
+ # @option opts [Symbol] :action Operation action,
83
+ # usually `:list`, `:create`, `:read`, `:update` or`:delete`.
84
+ #
85
+ # @return [Location] Location object.
86
+ #
87
+ def location(**opts)
88
+ service.location(self, **opts)
89
+ end
90
+
91
+ # @api private
92
+ def location_default_path(action, path)
93
+ case action
94
+ when :list, :create
95
+ path
96
+ when :read, :update, :delete
97
+ "#{path}/:id"
98
+ end
99
+ end
100
+ end
101
+
102
+ # Return URL for this resource. Resource if will be appended
103
+ # as suffix if present.
104
+ #
105
+ # @example
106
+ # user.new.url # => "http://users.srv.org/users"
107
+ #
108
+ # user = User.find 5
109
+ # Acfs.run
110
+ # user.url # => "http://users.srv.org/users/5"
111
+ #
112
+ # @return [ String ] Generated URL.
113
+ # @see ClassMethods#url
114
+ #
115
+ def url(**opts)
116
+ return nil if need_primary_key? && !primary_key?
117
+
118
+ self.class.service
119
+ .location(self.class, **opts, action: :read)
120
+ .build(attributes).str
121
+ end
122
+
123
+ # @api private
124
+ # Return true if resource needs a primary key (id) for singular actions.
125
+ def need_primary_key?
126
+ true
127
+ end
128
+
129
+ # @api private
130
+ # Return true if resource has a primary key (id) set.
131
+ def primary_key?
132
+ respond_to?(:id) && !id.nil?
133
+ end
134
+ end
135
+ end