apiql 0.2.2 → 0.3.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 (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