apiql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []