knuverse-knufactor 0.0.1
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +14 -0
- data/CONTRIBUTING.md +16 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +125 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/knuverse-knufactor.gemspec +42 -0
- data/lib/core_extensions/string/transformations.rb +28 -0
- data/lib/knuverse/knufactor.rb +42 -0
- data/lib/knuverse/knufactor/api_client.rb +30 -0
- data/lib/knuverse/knufactor/api_client_base.rb +112 -0
- data/lib/knuverse/knufactor/api_exception.rb +7 -0
- data/lib/knuverse/knufactor/exceptions/api_client_not_configured.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/immutable_modification.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/invalid_arguments.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/invalid_options.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/invalid_property.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/invalid_where_query.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/missing_path.rb +9 -0
- data/lib/knuverse/knufactor/exceptions/new_instance_with_id.rb +9 -0
- data/lib/knuverse/knufactor/helpers/api_client.rb +36 -0
- data/lib/knuverse/knufactor/helpers/resource.rb +112 -0
- data/lib/knuverse/knufactor/helpers/resource_class.rb +49 -0
- data/lib/knuverse/knufactor/resource.rb +207 -0
- data/lib/knuverse/knufactor/resource_collection.rb +189 -0
- data/lib/knuverse/knufactor/resources/client.rb +44 -0
- data/lib/knuverse/knufactor/simple_api_client.rb +20 -0
- data/lib/knuverse/knufactor/validations/api_client.rb +42 -0
- data/lib/knuverse/knufactor/validations/resource.rb +17 -0
- data/lib/knuverse/knufactor/version.rb +10 -0
- metadata +220 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module KnuVerse
|
2
|
+
module Knufactor
|
3
|
+
module Helpers
|
4
|
+
# Simple helper methods for the API Client
|
5
|
+
module APIClient
|
6
|
+
# Substitute characters with their JSON-supported versions
|
7
|
+
# @return [String]
|
8
|
+
def json_escape(s)
|
9
|
+
json_escape = {
|
10
|
+
'&' => '\u0026',
|
11
|
+
'>' => '\u003e',
|
12
|
+
'<' => '\u003c',
|
13
|
+
'%' => '\u0025',
|
14
|
+
"\u2028" => '\u2028',
|
15
|
+
"\u2029" => '\u2029'
|
16
|
+
}
|
17
|
+
json_escape_regex = /[\u2028\u2029&><%]/u
|
18
|
+
|
19
|
+
s.to_s.gsub(json_escape_regex, json_escape)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Provides access to the "raw" underlying rest-client
|
23
|
+
# @return [RestClient::Resource]
|
24
|
+
def raw
|
25
|
+
connection
|
26
|
+
end
|
27
|
+
|
28
|
+
# The API Client version (uses Semantic Versioning)
|
29
|
+
# @return [String]
|
30
|
+
def version
|
31
|
+
VERSION
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module KnuVerse
|
2
|
+
module Knufactor
|
3
|
+
module Helpers
|
4
|
+
# Simple helper methods for Resources
|
5
|
+
module Resource
|
6
|
+
def datetime_from_params(params, actual_key)
|
7
|
+
DateTime.new(
|
8
|
+
params["#{actual_key}(1i)"].to_i,
|
9
|
+
params["#{actual_key}(2i)"].to_i,
|
10
|
+
params["#{actual_key}(3i)"].to_i,
|
11
|
+
params["#{actual_key}(4i)"].to_i,
|
12
|
+
params["#{actual_key}(5i)"].to_i
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def fresh?
|
17
|
+
!tainted?
|
18
|
+
end
|
19
|
+
|
20
|
+
def id_property
|
21
|
+
self.class.properties.select { |_, opts| opts[:id_property] }.keys.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def id
|
25
|
+
@entity[id_property.to_s]
|
26
|
+
end
|
27
|
+
|
28
|
+
def immutable?
|
29
|
+
self.class.immutable?
|
30
|
+
end
|
31
|
+
|
32
|
+
# ActiveRecord ActiveModel::Name compatibility method
|
33
|
+
def model_name
|
34
|
+
self.class
|
35
|
+
end
|
36
|
+
|
37
|
+
def new?
|
38
|
+
!@entity.key?(id_property.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
def paths
|
42
|
+
self.class.paths
|
43
|
+
end
|
44
|
+
|
45
|
+
def path_for(kind)
|
46
|
+
self.class.path_for(kind)
|
47
|
+
end
|
48
|
+
|
49
|
+
# ActiveRecord ActiveModel::Model compatibility method
|
50
|
+
def persisted?
|
51
|
+
!new?
|
52
|
+
end
|
53
|
+
|
54
|
+
def properties
|
55
|
+
self.class.properties
|
56
|
+
end
|
57
|
+
|
58
|
+
def tainted?
|
59
|
+
@tainted ? true : false
|
60
|
+
end
|
61
|
+
|
62
|
+
# ActiveRecord ActiveModel::Conversion compatibility method
|
63
|
+
def to_key
|
64
|
+
new? ? [] : [id]
|
65
|
+
end
|
66
|
+
|
67
|
+
# ActiveRecord ActiveModel::Conversion compatibility method
|
68
|
+
def to_model
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
# ActiveRecord ActiveModel::Conversion compatibility method
|
73
|
+
def to_param
|
74
|
+
new? ? nil : id.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
# ActiveRecord ActiveModel compatibility method
|
78
|
+
def update(params)
|
79
|
+
new_params = {}
|
80
|
+
# need to convert multi-part datetime params
|
81
|
+
params.each do |key, value|
|
82
|
+
if key =~ /([^(]+)\(1i/
|
83
|
+
actual_key = key.match(/([^(]+)\(/)[1]
|
84
|
+
new_params[actual_key] = datetime_from_params(params, actual_key)
|
85
|
+
else
|
86
|
+
new_params[key] = value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
new_params.each do |key, value|
|
91
|
+
setter_key = "#{key}=".to_sym
|
92
|
+
raise Exceptions::InvalidProperty unless respond_to?(setter_key)
|
93
|
+
send(setter_key, value)
|
94
|
+
end
|
95
|
+
save
|
96
|
+
end
|
97
|
+
|
98
|
+
def <=>(other)
|
99
|
+
if id < other.id
|
100
|
+
-1
|
101
|
+
elsif id > other.id
|
102
|
+
1
|
103
|
+
elsif id == other.id
|
104
|
+
0
|
105
|
+
else
|
106
|
+
raise Exceptions::InvalidArguments
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module KnuVerse
|
2
|
+
module Knufactor
|
3
|
+
module Helpers
|
4
|
+
# Simple helper class methods for Resource
|
5
|
+
module ResourceClass
|
6
|
+
# Produce a more human-readable representation of {#i18n_key}
|
7
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
8
|
+
# @return [String]
|
9
|
+
def human
|
10
|
+
i18n_key.humanize
|
11
|
+
end
|
12
|
+
|
13
|
+
# Check if a resource class is immutable
|
14
|
+
def immutable?
|
15
|
+
@immutable ||= false
|
16
|
+
end
|
17
|
+
|
18
|
+
# A mock internationalization key based on the class name
|
19
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
20
|
+
# @return [String]
|
21
|
+
def i18n_key
|
22
|
+
name.split('::').last.to_underscore
|
23
|
+
end
|
24
|
+
|
25
|
+
alias singular_route_key i18n_key
|
26
|
+
|
27
|
+
# A symbolized version of {#i18n_key}
|
28
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
29
|
+
# @return [Symbol]
|
30
|
+
def param_key
|
31
|
+
i18n_key.to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
# All the properties defined for this Resource class
|
35
|
+
# @return [Hash{Symbol => Hash}]
|
36
|
+
def properties
|
37
|
+
@properties ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
# A route key for building URLs
|
41
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
42
|
+
# @return [String]
|
43
|
+
def route_key
|
44
|
+
i18n_key.en.plural
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module KnuVerse
|
2
|
+
module Knufactor
|
3
|
+
# A generic API resource
|
4
|
+
# TODO: Thread safety
|
5
|
+
class Resource
|
6
|
+
attr_accessor :client
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
include Comparable
|
10
|
+
include Validations::Resource
|
11
|
+
include Helpers::Resource
|
12
|
+
extend Helpers::ResourceClass
|
13
|
+
|
14
|
+
# Can this type of resource be changed client-side?
|
15
|
+
def self.immutable(status)
|
16
|
+
unless status.is_a?(TrueClass) || status.is_a?(FalseClass)
|
17
|
+
raise Exceptions::InvalidArguments
|
18
|
+
end
|
19
|
+
@immutable = status
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define a property for a model
|
23
|
+
# @!macro [attach] property
|
24
|
+
# The $1 property
|
25
|
+
# @todo add more validations on options and names
|
26
|
+
def self.property(name, options = {})
|
27
|
+
@properties ||= {}
|
28
|
+
|
29
|
+
invalid_prop_names = [
|
30
|
+
:>, :<, :'=', :class, :def,
|
31
|
+
:%, :'!', :/, :'.', :'?', :*, :'{}',
|
32
|
+
:'[]'
|
33
|
+
]
|
34
|
+
|
35
|
+
raise(Exceptions::InvalidProperty) if invalid_prop_names.include?(name.to_sym)
|
36
|
+
@properties[name.to_sym] = options
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the URI path for a resource method
|
40
|
+
def self.path(kind, uri)
|
41
|
+
paths[kind.to_sym] = uri
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create or set a class-level location to store URI paths for methods
|
45
|
+
# @return [Hash{Symbol => String}]
|
46
|
+
def self.paths
|
47
|
+
@paths ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.path_for(kind)
|
51
|
+
guess = kind.to_sym == :all ? route_key : "#{route_key}/#{kind}"
|
52
|
+
paths[kind.to_sym] || guess
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.gen_getter_method(name, opts)
|
56
|
+
method_name = opts[:type] == :boolean ? "#{name}?" : name
|
57
|
+
define_method(method_name.to_sym) do
|
58
|
+
name_as_string = name.to_s
|
59
|
+
reload if @lazy && !@entity.key?(name_as_string)
|
60
|
+
|
61
|
+
case opts[:type]
|
62
|
+
when :time
|
63
|
+
if @entity[name_as_string] && !@entity[name_as_string].to_s.empty?
|
64
|
+
Time.parse(@entity[name_as_string].to_s).utc
|
65
|
+
end
|
66
|
+
else
|
67
|
+
@entity[name_as_string]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.gen_setter_method(name, opts)
|
73
|
+
define_method("#{name}=".to_sym) do |value|
|
74
|
+
raise Exceptions::ImmutableModification if immutable?
|
75
|
+
# TODO: allow specifying a list of allowed values and validating against it
|
76
|
+
@entity[name.to_s] = opts[:type] == :time ? Time.parse(value.to_s).utc : value
|
77
|
+
@tainted = true
|
78
|
+
@modified_properties << name.to_sym
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.gen_property_methods
|
83
|
+
properties.each do |prop, opts|
|
84
|
+
# Getter methods
|
85
|
+
next if opts[:id_property]
|
86
|
+
gen_getter_method(prop, opts) unless opts[:write_only]
|
87
|
+
|
88
|
+
# Setter methods (don't make one for obviously read-only properties)
|
89
|
+
gen_setter_method(prop, opts) unless opts[:read_only]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.all(options = {})
|
94
|
+
# TODO: Add validations for options
|
95
|
+
|
96
|
+
# TODO: add validation checks for the required pieces
|
97
|
+
raise Exceptions::MissingPath unless path_for(:all)
|
98
|
+
|
99
|
+
api_client = options[:api_client] || APIClient.instance
|
100
|
+
|
101
|
+
root = name.split('::').last.en.plural.to_underscore
|
102
|
+
# TODO: do something with lazy requests...
|
103
|
+
|
104
|
+
ResourceCollection.new(
|
105
|
+
api_client.get(path_for(:all))[root].collect do |record|
|
106
|
+
new(
|
107
|
+
entity: record,
|
108
|
+
lazy: (options[:lazy] ? true : false),
|
109
|
+
tainted: false,
|
110
|
+
api_client: api_client
|
111
|
+
)
|
112
|
+
end,
|
113
|
+
type: self,
|
114
|
+
api_client: api_client
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.get(id, options = {})
|
119
|
+
# TODO: Add validations for options
|
120
|
+
raise Exceptions::MissingPath unless path_for(:all)
|
121
|
+
|
122
|
+
api_client = options[:api_client] || APIClient.instance
|
123
|
+
|
124
|
+
new(
|
125
|
+
entity: api_client.get("#{path_for(:all)}/#{id}"),
|
126
|
+
lazy: false,
|
127
|
+
tainted: false,
|
128
|
+
api_client: api_client
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.where(attribute, value, options = {})
|
133
|
+
# TODO: validate incoming options
|
134
|
+
options[:comparison] ||= value.is_a?(Regexp) ? :match : '=='
|
135
|
+
api_client = options[:api_client] || APIClient.instance
|
136
|
+
all(lazy: (options[:lazy] ? true : false), api_client: api_client).where(
|
137
|
+
attribute, value, comparison: options[:comparison]
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
def initialize(options = {})
|
142
|
+
# TODO: better options validations
|
143
|
+
raise Exceptions::InvalidOptions unless options.is_a?(Hash)
|
144
|
+
|
145
|
+
@entity = options[:entity] || {}
|
146
|
+
|
147
|
+
# Allows lazy-loading if we're told this is a lazy instance
|
148
|
+
# This means only the minimal attributes were fetched.
|
149
|
+
# This shouldn't be set by end-users.
|
150
|
+
@lazy = options.key?(:lazy) ? options[:lazy] : false
|
151
|
+
# This allows local, user-created instances to be differentiated from fetched
|
152
|
+
# instances from the backend API. This shouldn't be set by end-users.
|
153
|
+
@tainted = options.key?(:tainted) ? options[:tainted] : true
|
154
|
+
# This is the API Client used to get data for this resource
|
155
|
+
@api_client = options[:api_client] || APIClient.instance
|
156
|
+
@errors = {}
|
157
|
+
# A place to store which properties have been modified
|
158
|
+
@modified_properties = []
|
159
|
+
|
160
|
+
validate_mutability
|
161
|
+
validate_id
|
162
|
+
|
163
|
+
self.class.class_eval { gen_property_methods }
|
164
|
+
end
|
165
|
+
|
166
|
+
def destroy
|
167
|
+
raise Exceptions::ImmutableModification if immutable?
|
168
|
+
unless new?
|
169
|
+
@api_client.delete("#{path_for(:all)}/#{id}")
|
170
|
+
@lazy = false
|
171
|
+
@tainted = true
|
172
|
+
@entity.delete('id')
|
173
|
+
end
|
174
|
+
true
|
175
|
+
end
|
176
|
+
|
177
|
+
def reload
|
178
|
+
if new?
|
179
|
+
# Can't reload a new resource
|
180
|
+
false
|
181
|
+
else
|
182
|
+
@entity = @api_client.get("#{path_for(:all)}/#{id}")
|
183
|
+
@lazy = false
|
184
|
+
@tainted = false
|
185
|
+
true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def save
|
190
|
+
saveable_data = @entity.select do |prop, value|
|
191
|
+
pr = prop.to_sym
|
192
|
+
go = properties.key?(pr) && !properties[pr][:read_only] && !value.nil?
|
193
|
+
@modified_properties.uniq.include?(pr) if go
|
194
|
+
end
|
195
|
+
|
196
|
+
if new?
|
197
|
+
@entity = @api_client.post(path_for(:all).to_s, saveable_data)
|
198
|
+
@lazy = true
|
199
|
+
else
|
200
|
+
@api_client.put("#{path_for(:all)}/#{id}", saveable_data)
|
201
|
+
end
|
202
|
+
@tainted = false
|
203
|
+
true
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|