apiql 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +39 -2
  3. data/apiql.gemspec +1 -1
  4. data/lib/apiql.rb +101 -53
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f103af812fb631ddf00a34acc478436d42d39fd1a66de8e2b0ee32a7023d159c
4
- data.tar.gz: 2cf523c953d6ba31d3017183fe09353546a9d627869972e325342e732c7f1f6c
3
+ metadata.gz: 3f34e609be9a7132527a772ee95d0e7dd4e43aff4bb7d8f9fd43010f73e9632e
4
+ data.tar.gz: 1194eed934b4c0f26084dc0cdec5beac7d51967236e1b6438efa176d176cfdb2
5
5
  SHA512:
6
- metadata.gz: 765875ac74538f510065377ffe052d0e5a75ed2b331b9883e142be064b57f76c74c9398afe4c001e3e9674a94d7844aa8e940d819f336c0589e0e13038ed7020
7
- data.tar.gz: 4d82c9d04123de11ae47da1eaca5b0d2ecce4d212b1e828ec6b19a35f6a75e0332fae273a4c9368a9eb28494a9a64d2bceb1bf04230a7ac6899ead8d2343c035
6
+ metadata.gz: adca6b4acf505f3a281a352640dbabb23a05b9ded7a9ddd99c10d64f0cbba1b67b054a66e048cbdd0f59c164f9fa2b850c3f8693cf0ec92ad967326e87639ee1
7
+ data.tar.gz: 0c1c05a98a7cb45868e3d0f3118b4d4ccf87c475e9e88aa84cb49808005ee5f45cdc1df9595eef0be5f5c3f7087213f8ecaeca97e5fad45733dbdf8280a28677
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # APIQL
2
2
 
3
- Implementation of the API language similar to GraphQL for Ruby on Rails
3
+ Implementation of the API language similar to GraphQL for Ruby on Rails.
4
+
5
+ It compiles requests into Hashes for faster rendering.
6
+
7
+ Now, it automatically detects nested entities and eager-loads them for faster DB access!
4
8
 
5
9
  Define your responder (requested methods):
6
10
 
@@ -102,6 +106,37 @@ logout() {
102
106
 
103
107
  ```
104
108
 
109
+ you can call methods on entities:
110
+
111
+ ```javascript
112
+ apiql("user", `
113
+ authenticate(email, password) {
114
+ token
115
+ }
116
+
117
+ me.reload {
118
+ email full_name role token
119
+
120
+ roles(filter) {
121
+ id title
122
+ }
123
+
124
+ contacts.primary {
125
+ full_name
126
+ email
127
+ }
128
+ }
129
+ `, {
130
+ email: email,
131
+ filter: 'all',
132
+ password: password
133
+ })
134
+ .then(response => {
135
+ let user = response['me.reload'] // name in response equals to called
136
+ })
137
+ }
138
+ ```
139
+
105
140
  You can use initializer like this for authorization using cancancan gem:
106
141
 
107
142
  config/initializers/apiql.rb:
@@ -126,9 +161,11 @@ class APIQL
126
161
  end
127
162
  end
128
163
  end
164
+ ```
129
165
 
130
- and authorize access to every entity:
166
+ and even authorize access to every entity:
131
167
 
168
+ ```ruby
132
169
  class ApplicationRecord < ActiveRecord::Base
133
170
  class BaseEntity < APIQL::Entity
134
171
  def initialize(object, context)
data/apiql.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'apiql'
7
- spec.version = '0.2.2'
7
+ spec.version = '0.3.0'
8
8
  spec.authors = ['Dmitry Silchenko']
9
9
  spec.email = ['dmitry@desofto.com']
10
10
 
data/lib/apiql.rb CHANGED
@@ -20,13 +20,14 @@ class APIQL
20
20
  request = params[:apiql_request]
21
21
 
22
22
  if request.present?
23
- redis&.set("api-ql-cache-#{request_id}", request)
23
+ request = compile(request)
24
+ redis&.set("api-ql-cache-#{request_id}", request.to_json)
24
25
  @@cache = {} if @@cache.count > 1000
25
26
  @@cache[request_id] = request
26
27
  else
27
28
  request = @@cache[request_id]
28
- request ||= redis&.get("api-ql-cache-#{request_id}")
29
- raise CacheMissed unless request.present?
29
+ request ||= JSON.parse(redis.get("api-ql-cache-#{request_id}")) rescue nil
30
+ raise CacheMissed unless request.present? && request.is_a?(::Array)
30
31
  end
31
32
 
32
33
  request
@@ -50,74 +51,97 @@ class APIQL
50
51
  nil
51
52
  end
52
53
  end
53
- end
54
54
 
55
- def initialize(binder, *fields)
56
- @context = ::APIQL::Context.new(binder, *fields)
57
- @context.inject_delegators(self)
58
- end
55
+ def compile(schema)
56
+ result = []
59
57
 
60
- def render(schema)
61
- result = {}
58
+ ptr = result
59
+ pool = []
62
60
 
63
- function = nil
64
- data = nil
61
+ while schema.present? do
62
+ if reg = schema.match(/\A\s*\{(?<rest>.*)\z/m) # {
63
+ schema = reg[:rest]
65
64
 
66
- pool = nil
67
- keys = nil
68
- last_key = nil
65
+ pool.push(ptr)
66
+ key = ptr.pop
67
+ ptr.push(key => (ptr = []))
68
+ elsif reg = schema.match(/\A\s*\}(?<rest>.*)\z/m) # }
69
+ schema = reg[:rest]
69
70
 
70
- while schema.present? do
71
- if reg = schema.match(/\A\s*\{(?<rest>.*)\z/m) # {
72
- schema = reg[:rest]
71
+ ptr = pool.pop
72
+ elsif pool.any? && (reg = schema.match(/\A\s*(?<name>[\w\.]+)(\((?<params>.*)\))?(?<rest>.*)\z/m))
73
+ schema = reg[:rest]
73
74
 
74
- pool.push [keys, last_key]
75
- keys = []
76
- elsif reg = schema.match(/\A\s*\}(?<rest>.*)\z/m) # }
77
- schema = reg[:rest]
75
+ if reg[:params].nil?
76
+ key = reg[:name]
77
+ else
78
+ key = "#{reg[:name]}(#{reg[:params]})"
79
+ end
78
80
 
79
- last_keys = keys
81
+ ptr.push(key)
82
+ elsif reg = schema.match(/\A\s*(?<name>[\w\.]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\{(?<rest>.*)\z/m)
83
+ schema = reg[:rest]
80
84
 
81
- keys, last_key = pool.pop
85
+ pool.push(ptr)
86
+ ptr.push("#{reg[:name]}(#{reg[:params]})" => (ptr = []))
87
+ elsif reg = schema.match(/\A\s*(?<name>[\w\.]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\n?(?<rest>.*)\z/m)
88
+ schema = reg[:rest]
82
89
 
83
- if pool.empty?
84
- result[function] = @context.render_value(data, last_keys)
85
- function = nil
90
+ ptr.push("#{reg[:name]}(#{reg[:params]})")
86
91
  else
87
- keys.delete(last_key)
88
- keys << { last_key => last_keys }
92
+ raise Error, schema
89
93
  end
90
- elsif function.present? && (reg = schema.match(/\A\s*(?<name>[\w\.]+)(\((?<params>.*)\))?(?<rest>.*)\z/m))
91
- schema = reg[:rest]
94
+ end
92
95
 
93
- if reg[:params].present?
94
- keys << [reg[:name], reg[:params]]
95
- else
96
- keys << reg[:name]
97
- end
96
+ result
97
+ end
98
+ end
98
99
 
99
- last_key = reg[:name]
100
- elsif reg = schema.match(/\A\s*(?<name>[\w\.]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\{(?<rest>.*)\z/m)
101
- schema = reg[:rest]
100
+ def initialize(binder, *fields)
101
+ @context = ::APIQL::Context.new(binder, *fields)
102
+ @context.inject_delegators(self)
103
+ end
102
104
 
103
- function = reg[:name]
104
- params = @context.parse_params(reg[:params])
105
+ def eager_load
106
+ result = @eager_load
105
107
 
106
- data = public_send(function, *params)
108
+ @eager_load = nil
107
109
 
108
- pool = []
109
- requested = {}
110
+ result
111
+ end
110
112
 
111
- last_key = nil
113
+ def render(schema)
114
+ result = {}
112
115
 
113
- pool.push [keys, last_key]
114
- keys = []
115
- elsif reg = schema.match(/\A\s*(?<name>[\w\.]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\n?(?<rest>.*)\z/m)
116
- schema = reg[:rest]
116
+ schema.map do |call|
117
+ if call.is_a? ::Hash
118
+ call.each do |function, sub_schema|
119
+ reg = function.match(/\A(?<name>[\w\.]+)(\((?<params>.*)\))?\z/)
120
+ raise Error, function unless reg.present?
121
+
122
+ function = reg[:name]
123
+ params = @context.parse_params(reg[:params])
124
+
125
+ @eager_load = eager_loads(sub_schema)
126
+ data = public_send(function, *params)
127
+ if @eager_load.present? && !data.is_a?(::Hash)
128
+ if data.respond_to?(:each) && data.respond_to?(:map)
129
+ data = data.eager_load(eager_load)
130
+ elsif data.respond_to?(:id)
131
+ data = data.class.eager_load(eager_load).find(data.id)
132
+ end
133
+ end
134
+
135
+ result[function] = @context.render_value(data, sub_schema)
136
+ end
137
+ else
138
+ reg = call.match(/\A(?<name>[\w\.]+)(\((?<params>.*)\))?\z/)
139
+ raise Error, call unless reg.present?
117
140
 
118
141
  function = reg[:name]
119
142
  params = @context.parse_params(reg[:params])
120
143
 
144
+ @eager_load = ''
121
145
  data = public_send(function, *params)
122
146
  if data.is_a? Array
123
147
  if data.any? { |item| !APIQL::simple_class?(item) }
@@ -128,10 +152,30 @@ class APIQL
128
152
  end
129
153
 
130
154
  result[function] = data
155
+ end
156
+ end
131
157
 
132
- function = nil
133
- else
134
- raise Error
158
+ result
159
+ end
160
+
161
+ private
162
+
163
+ def eager_loads(schema)
164
+ result = []
165
+
166
+ schema.each do |call|
167
+ if call.is_a? Hash
168
+ call.each do |function, sub_schema|
169
+ next if function.include? '('
170
+ function = function.split('.').first if function.include? '.'
171
+
172
+ sub = eager_loads(sub_schema)
173
+ if sub.present?
174
+ result.push(function => sub)
175
+ else
176
+ result.push function
177
+ end
178
+ end
135
179
  end
136
180
  end
137
181
 
@@ -316,7 +360,11 @@ class APIQL
316
360
  begin
317
361
  "#{value.class.name}::Entity".constantize.new(value, self).render(schema)
318
362
  rescue StandardError
319
- nil
363
+ if ::Rails.env.development?
364
+ raise
365
+ else
366
+ nil
367
+ end
320
368
  end
321
369
  end
322
370
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apiql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Silchenko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-16 00:00:00.000000000 Z
11
+ date: 2018-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler