apiql 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1247c3ec8750be3ff5db48d0503782f5342bb42dd7f0159518eb214356886697
4
+ data.tar.gz: 2f15a8174f0d452878913bf352ef7b49558bb33610fa63f33fc5bd4daa938519
5
+ SHA512:
6
+ metadata.gz: 7ce244c93ce695ec8cfd56bc7f11f055312f1b59e0220a193faeb8a1a597b88733a33e8c0a2ca96e198fd01b34f15ff323bb51a6f03d715ce0ac02c643e36b84
7
+ data.tar.gz: c3edf30b166525a052be905902b2f4d7641e38a099e20ac75951e40c9d2dadf24cc4f8894012c9fbe7cd07a3f3d6a0deae4d4f52c90655408f457ca682d6cb3e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 DeSofto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
1
+ # APIQL
2
+
3
+ Implementation of the API language similar to GraphQL for Ruby on Rails
4
+
5
+ Define your responder (requested methods):
6
+
7
+ ```ruby
8
+ class UserAPIQL < ::APIQL
9
+ def me
10
+ authorize! :show, ::User
11
+
12
+ context.current_user&.eager_load(:roles) # you can preload everything you need for speedup presenters
13
+ end
14
+
15
+ def authenticate(email, password)
16
+ user = ::User.find_by(email)
17
+ user.authenticate(password)
18
+
19
+ user
20
+ end
21
+
22
+ def logout
23
+ context.current_user&.logout
24
+
25
+ :ok
26
+ end
27
+ end
28
+
29
+ ```
30
+ In controller or Grape API endpoint, handler of POST /user method:
31
+
32
+ ```ruby
33
+ def user
34
+ schema = APIQL.cache(params)
35
+ UserAPIQL.new(self, :session, :current_user, :params).render(schema)
36
+ end
37
+
38
+ ```
39
+ variables `session`, `current_user` and `params` will be stored into context you can use in presenters and handlers
40
+
41
+ Define presenters for your models:
42
+
43
+ ```ruby
44
+ class User < ApplicationRecord
45
+ class Entity < ::APIQL::Entity
46
+ attributes :full_name, :email, :token, :role, :roles
47
+
48
+ def token
49
+ object.token if object == context.current_user
50
+ end
51
+ end
52
+
53
+ has_many :roles
54
+
55
+ ...
56
+ end
57
+
58
+ ```
59
+ # JS:
60
+
61
+ application.js:
62
+
63
+ ```javascript
64
+ APIQL.endpoint = "/"
65
+ ```
66
+
67
+ ```javascript
68
+ authenticate(email, password) {
69
+ let api = new APIQL("user")
70
+ api.call(`
71
+ logout()
72
+ authenticate(email,password) {
73
+ token
74
+ }
75
+ me {
76
+ email full_name role token
77
+ }
78
+ `, {
79
+ email: email,
80
+ password: password
81
+ })
82
+ .then(response => {
83
+ let user = response.me
84
+ })
85
+ }
86
+
87
+ logout() {
88
+ let api = new APIQL("user")
89
+ api.call(`
90
+ logout
91
+ `)
92
+ .then(response => {
93
+ })
94
+ }
95
+
96
+ ```
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'apiql'
7
+ spec.version = '0.1.0'
8
+ spec.authors = ['Dmitry Silchenko']
9
+ spec.email = ['dmitry@desofto.com']
10
+
11
+ spec.summary = 'APIQL.'
12
+ spec.description = 'APIQL.'
13
+ spec.homepage = 'https://github.com/desofto/apiql'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.11'
22
+ spec.add_development_dependency 'rake', '~> 11.0'
23
+ spec.add_development_dependency 'pry', '~> 0.10'
24
+ end
@@ -0,0 +1,64 @@
1
+ class APIQL {
2
+ constructor(endpoint) {
3
+ this.endpoint = endpoint
4
+ }
5
+
6
+ hash(s) {
7
+ let hash = 0, i, chr
8
+ if(s.length === 0) return hash
9
+ for(i = 0; i < s.length; i++) {
10
+ chr = s.charCodeAt(i)
11
+ hash = ((hash << 5) - hash) + chr
12
+ hash |= 0
13
+ }
14
+ return hash
15
+ }
16
+
17
+ call(schema, params = {}, form = null) {
18
+ return new Promise((resolve, reject) => {
19
+ if(params instanceof FormData) {
20
+ form = params
21
+ params = {}
22
+ }
23
+
24
+ if(form) {
25
+ Object.keys(params).forEach(key => {
26
+ form.append(key, params[key])
27
+ })
28
+ }
29
+
30
+ if(form) {
31
+ form.append('apiql', this.hash(schema))
32
+ } else {
33
+ params.apiql = this.hash(schema)
34
+ }
35
+
36
+ Vue.http.post(`${APIQL.endpoint}${this.endpoint}`, params)
37
+ .then(response => {
38
+ resolve(response.body)
39
+ })
40
+ .catch(response => {
41
+ if(response.status == 401 && APIQL.unauthenticated) {
42
+ APIQL.unauthenticated()
43
+ }
44
+
45
+ if(form) {
46
+ form.append('apiql_request', schema)
47
+ } else {
48
+ params.apiql_request = schema
49
+ }
50
+
51
+ Vue.http.post(`${APIQL.endpoint}${this.endpoint}`, form || params)
52
+ .then(response => {
53
+ resolve(response.body)
54
+ })
55
+ .catch(response => {
56
+ alert(response.body.errors)
57
+ })
58
+ })
59
+ })
60
+ }
61
+ }
62
+
63
+ APIQL.unauthenticated = null
64
+ APIQL.endpoint = ''
@@ -0,0 +1,275 @@
1
+ class APIQL
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ initializer 'apiql.assets' do |app|
5
+ app.config.assets.paths << root.join('assets', 'javascripts').to_s
6
+ end
7
+ end
8
+ end
9
+
10
+ class Error < StandardError; end
11
+ class CacheMissed < StandardError; end
12
+
13
+ attr_reader :context
14
+
15
+ class << self
16
+ @@cache = {}
17
+
18
+ def cache(params)
19
+ request_id = params[:apiql]
20
+ request = params[:apiql_request]
21
+
22
+ if request.present?
23
+ redis&.set("api-ql-cache-#{request_id}", request)
24
+ @@cache = {} if @@cache.count > 1000
25
+ @@cache[request_id] = request
26
+ else
27
+ request = @@cache[request_id]
28
+ request ||= redis&.get("api-ql-cache-#{request_id}")
29
+ raise CacheMissed unless request.present?
30
+ end
31
+
32
+ request
33
+ end
34
+
35
+ def simple_class?(value)
36
+ value.nil? ||
37
+ value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
38
+ value.is_a?(Symbol) || value.is_a?(String) ||
39
+ value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(BigDecimal)
40
+ end
41
+
42
+ private
43
+
44
+ def redis
45
+ @redis ||=
46
+ begin
47
+ ::Redis.new(host: 'localhost')
48
+ rescue
49
+ nil
50
+ end
51
+ end
52
+ end
53
+
54
+ def initialize(binder, *fields)
55
+ @context = ::APIQL::Context.new(binder, *fields)
56
+ end
57
+
58
+ def render(schema)
59
+ result = {}
60
+
61
+ function = nil
62
+ data = nil
63
+
64
+ pool = nil
65
+ keys = nil
66
+ last_key = nil
67
+
68
+ while schema.present? do
69
+ if reg = schema.match(/\A\s*\{(?<rest>.*)\z/m) # {
70
+ schema = reg[:rest]
71
+
72
+ pool.push [keys, last_key]
73
+ keys = []
74
+ elsif reg = schema.match(/\A\s*\}(?<rest>.*)\z/m) # }
75
+ schema = reg[:rest]
76
+
77
+ last_keys = keys
78
+
79
+ keys, last_key = pool.pop
80
+
81
+ if pool.empty?
82
+ result[function] = context.render_value(data, last_keys)
83
+ function = nil
84
+ else
85
+ keys.delete(last_key)
86
+ keys << { last_key => last_keys }
87
+ end
88
+ elsif function.present? && (reg = schema.match(/\A\s*(?<name>\w+)(\((?<params>.*)\))?(?<rest>.*)\z/m))
89
+ schema = reg[:rest]
90
+
91
+ if reg[:params].present?
92
+ keys << [reg[:name], reg[:params]]
93
+ else
94
+ keys << reg[:name]
95
+ end
96
+
97
+ last_key = reg[:name]
98
+ elsif reg = schema.match(/\A\s*(?<name>\w+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\{(?<rest>.*)\z/m)
99
+ schema = reg[:rest]
100
+
101
+ function = reg[:name]
102
+ params = context.parse_params(reg[:params])
103
+
104
+ data = public_send(function, *params)
105
+
106
+ pool = []
107
+ requested = {}
108
+
109
+ last_key = nil
110
+
111
+ pool.push [keys, last_key]
112
+ keys = []
113
+ elsif reg = schema.match(/\A\s*(?<name>\w+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\n(?<rest>.*)\z/m)
114
+ schema = reg[:rest]
115
+
116
+ function = reg[:name]
117
+ params = context.parse_params(reg[:params])
118
+
119
+ data = public_send(function, *params)
120
+ unless APIQL::simple_class?(data)
121
+ data = nil
122
+ end
123
+
124
+ result[function] = data
125
+
126
+ function = nil
127
+ else
128
+ raise Error
129
+ end
130
+ end
131
+
132
+ result
133
+ end
134
+
135
+ class Entity
136
+ class << self
137
+ attr_reader :apiql_attributes
138
+
139
+ def inherited(child)
140
+ super
141
+
142
+ return if self.class == ::APIQL::Entity
143
+
144
+ attributes = apiql_attributes&.try(:deep_dup) || []
145
+
146
+ child.instance_eval do
147
+ @apiql_attributes = attributes
148
+ end
149
+ end
150
+
151
+ def attributes(*attrs)
152
+ @apiql_attributes ||= []
153
+ @apiql_attributes += attrs.map(&:to_sym)
154
+ end
155
+ end
156
+
157
+ attr_reader :object, :context
158
+
159
+ def initialize(object, context)
160
+ @object = object
161
+ @context = context
162
+ end
163
+
164
+ def render(schema = nil)
165
+ return unless @object.present?
166
+
167
+ respond = {}
168
+
169
+ schema.each do |field|
170
+ if field.is_a? Hash
171
+ field.each do |field, sub_schema|
172
+ name = field.is_a?(Array) ? field.first : field
173
+ respond[name] = render_attribute(field, sub_schema)
174
+ end
175
+ else
176
+ name = field.is_a?(Array) ? field.first : field
177
+ respond[name] = render_attribute(field)
178
+ end
179
+ end
180
+
181
+ respond
182
+ end
183
+
184
+ private
185
+
186
+ def get_field(field)
187
+ if field.is_a? Array
188
+ field, params = field
189
+ params = context.parse_params(params)
190
+ end
191
+ return unless field.to_sym.in? self.class.apiql_attributes
192
+
193
+ if respond_to? field
194
+ public_send(field, *params)
195
+ else
196
+ object.public_send(field, *params)
197
+ end
198
+ end
199
+
200
+ def render_attribute(field, schema = nil)
201
+ data = get_field(field)
202
+
203
+ if data.is_a?(Hash) && schema.present?
204
+ respond = {}
205
+
206
+ schema.each do |field|
207
+ if field.is_a? Hash
208
+ field.each do |field, sub_schema|
209
+ respond[field] = render_value(data[field.to_sym], sub_schema)
210
+ end
211
+ else
212
+ respond[field] = render_value(data[field.to_sym], schema)
213
+ end
214
+ end
215
+
216
+ respond
217
+ else
218
+ render_value(data, schema)
219
+ end
220
+ end
221
+
222
+ def render_value(value, schema)
223
+ if schema.present?
224
+ context.render_value(value, schema)
225
+ else
226
+ value
227
+ end
228
+ end
229
+ end
230
+
231
+ class HashEntity < Entity
232
+ def get_field(field)
233
+ object[field.to_sym]
234
+ end
235
+ end
236
+
237
+ class Context
238
+ def initialize(binder, *fields)
239
+ fields.each do |field|
240
+ instance_variable_set("@#{field}", binder.send(field))
241
+ class_eval do
242
+ attr_accessor field
243
+ end
244
+ end
245
+ end
246
+
247
+ def parse_params(list)
248
+ list&.split(',')&.map(&:strip)&.map do |name|
249
+ if reg = name.match(/\A[a-zA-Z]\w*\z/)
250
+ params[name]
251
+ else
252
+ begin
253
+ Float(name)
254
+ rescue
255
+ name
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ def render_value(value, schema)
262
+ if value.is_a? Hash
263
+ HashEntity.new(value, self).render(schema)
264
+ elsif value.respond_to?(:each) && value.respond_to?(:map)
265
+ value.map do |object|
266
+ "#{object.class.name}::Entity".constantize.new(object, self).render(schema)
267
+ end
268
+ elsif APIQL::simple_class?(value)
269
+ value
270
+ else
271
+ "#{value.class.name}::Entity".constantize.new(value, self).render(schema)
272
+ end
273
+ end
274
+ end
275
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apiql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmitry Silchenko
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ description: APIQL.
56
+ email:
57
+ - dmitry@desofto.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - README.md
64
+ - apiql.gemspec
65
+ - assets/javascripts/apiql.js
66
+ - lib/apiql.rb
67
+ homepage: https://github.com/desofto/apiql
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 2.7.6
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: APIQL.
91
+ test_files: []