joshua 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.
- checksums.yaml +7 -0
- data/.version +1 -0
- data/lib/.DS_Store +0 -0
- data/lib/doc/doc.rb +273 -0
- data/lib/doc/special.rb +20 -0
- data/lib/joshua.rb +39 -0
- data/lib/joshua/base.rb +312 -0
- data/lib/joshua/error.rb +61 -0
- data/lib/joshua/opts.rb +204 -0
- data/lib/joshua/params/define.rb +53 -0
- data/lib/joshua/params/parse.rb +56 -0
- data/lib/joshua/params/types.rb +152 -0
- data/lib/joshua/params/types_errors.rb +33 -0
- data/lib/joshua/response.rb +86 -0
- data/lib/misc/api_example.coffee +75 -0
- data/lib/misc/doc.css +29 -0
- data/lib/misc/doc.js +279 -0
- data/lib/misc/favicon.png +0 -0
- data/lib/misc/ruby_client.rb +52 -0
- metadata +88 -0
data/lib/joshua/error.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class Joshua
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
RESCUE_FROM = {}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# rescue_from CustomError do ...
|
9
|
+
# for unhandled
|
10
|
+
# rescue_from :all do
|
11
|
+
# api.error 500, 'Error happens'
|
12
|
+
# end
|
13
|
+
# define handled error code and description
|
14
|
+
# error :not_found, 'Document not found'
|
15
|
+
# error 404, 'Document not found'
|
16
|
+
# in api methods
|
17
|
+
# error 404
|
18
|
+
# error :not_found
|
19
|
+
def rescue_from klass, desc=nil, &block
|
20
|
+
RESCUE_FROM[klass] = desc || block
|
21
|
+
end
|
22
|
+
|
23
|
+
# show and render single error in class error format
|
24
|
+
# usually when API class not found
|
25
|
+
def error text
|
26
|
+
out = Response.new nil
|
27
|
+
out.error text
|
28
|
+
out.render
|
29
|
+
end
|
30
|
+
|
31
|
+
def error_print error
|
32
|
+
return if ENV['RACK_ENV'] == 'test'
|
33
|
+
|
34
|
+
puts
|
35
|
+
puts 'Joshua error dump'.red
|
36
|
+
puts '---'
|
37
|
+
puts '%s: %s' % [error.class, error.message]
|
38
|
+
puts '---'
|
39
|
+
puts error.backtrace
|
40
|
+
puts '---'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
###
|
45
|
+
|
46
|
+
def error desc
|
47
|
+
if err = RESCUE_FROM[desc]
|
48
|
+
if err.is_a?(Proc)
|
49
|
+
err.call
|
50
|
+
else
|
51
|
+
response.error desc, err
|
52
|
+
desc = err
|
53
|
+
end
|
54
|
+
|
55
|
+
return
|
56
|
+
end
|
57
|
+
|
58
|
+
raise Joshua::Error, desc
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
data/lib/joshua/opts.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
class Joshua
|
2
|
+
PARAMS ||= Params::Define.new
|
3
|
+
ANNOTATIONS ||= {}
|
4
|
+
OPTS = {}
|
5
|
+
PLUGINS = {}
|
6
|
+
DOCUMENTED = []
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def base what
|
10
|
+
set :opts, :base, what
|
11
|
+
end
|
12
|
+
|
13
|
+
# if you want to make API DOC public use "documented"
|
14
|
+
def documented
|
15
|
+
if self == Joshua
|
16
|
+
DOCUMENTED.map(&:to_s).sort.map(&:constantize)
|
17
|
+
else
|
18
|
+
DOCUMENTED.push self unless DOCUMENTED.include?(self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def api_path
|
23
|
+
to_s.underscore.sub(/_api$/, '')
|
24
|
+
end
|
25
|
+
|
26
|
+
# define method annotations
|
27
|
+
# annotation :unsecure! do
|
28
|
+
# @is_unsecure = true
|
29
|
+
# end
|
30
|
+
# unsecure!
|
31
|
+
# def login
|
32
|
+
# ...
|
33
|
+
def annotation name, &block
|
34
|
+
ANNOTATIONS[name] = block
|
35
|
+
self.define_singleton_method name do |*args|
|
36
|
+
PARAMS.__add_annotation name, args
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# /api/companies/1/show
|
41
|
+
def member &block
|
42
|
+
@method_type = :member
|
43
|
+
class_exec &block
|
44
|
+
@method_type = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# /api/companies/list?countrty_id=1
|
48
|
+
def collection &block
|
49
|
+
@method_type = :collection
|
50
|
+
class_exec &block
|
51
|
+
@method_type = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# There are multiple ways to create params
|
55
|
+
# params :name, String, req: true
|
56
|
+
# params.name!, String
|
57
|
+
# params do
|
58
|
+
# name String, required: true
|
59
|
+
# name! String
|
60
|
+
# end
|
61
|
+
# params :label do |value, opts|
|
62
|
+
# # validate is value a label, return coarced label
|
63
|
+
# # or raise error with error
|
64
|
+
# end
|
65
|
+
def params *args, &block
|
66
|
+
if name = args.first
|
67
|
+
if block
|
68
|
+
# if argument is provided we create a validator
|
69
|
+
Params::Parse.define name, &block
|
70
|
+
else
|
71
|
+
only_in_api_methods!
|
72
|
+
PARAMS.send *args
|
73
|
+
end
|
74
|
+
elsif block
|
75
|
+
PARAMS.instance_eval &block
|
76
|
+
else
|
77
|
+
only_in_api_methods!
|
78
|
+
PARAMS
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# api method icon
|
83
|
+
# you can find great icons at https://boxicons.com/ - export to svg
|
84
|
+
def icon data
|
85
|
+
if @method_type
|
86
|
+
raise ArgumentError.new('Icons cant be added on methods')
|
87
|
+
else
|
88
|
+
set :opts, :icon, data
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# api method description
|
93
|
+
def desc data
|
94
|
+
if @method_type
|
95
|
+
PARAMS.__add :desc, data
|
96
|
+
else
|
97
|
+
set :opts, :desc, data
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# api method detailed description
|
102
|
+
def detail data
|
103
|
+
return if data.to_s == ''
|
104
|
+
|
105
|
+
if @method_type
|
106
|
+
PARAMS.__add :detail, data
|
107
|
+
else
|
108
|
+
set :opts, :detail, data
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# method in available for GET requests as well
|
113
|
+
def gettable
|
114
|
+
if @method_type
|
115
|
+
PARAMS.__add :gettable
|
116
|
+
else
|
117
|
+
raise ArgumentError.new('gettable can only be set on methods')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# allow methods without @api.bearer token set
|
122
|
+
def unsafe
|
123
|
+
if @method_type
|
124
|
+
PARAMS.__add :unsafe
|
125
|
+
else
|
126
|
+
raise ArgumentError.new('Only api methods can be unsafe')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# all api methods are secure (require bearer token)
|
131
|
+
def unsecure
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
# block execute before any public method or just some member or collection methods
|
136
|
+
def before &block
|
137
|
+
set_callback :before, block
|
138
|
+
end
|
139
|
+
|
140
|
+
# block execute after any public method or just some member or collection methods
|
141
|
+
# used to add meta tags to response
|
142
|
+
def after &block
|
143
|
+
set_callback :after, block
|
144
|
+
end
|
145
|
+
|
146
|
+
# simplified module include, masked as plugin
|
147
|
+
# Joshua.plugin :foo do ...
|
148
|
+
# Joshua.plugin :foo
|
149
|
+
def plugin name, &block
|
150
|
+
if block_given?
|
151
|
+
# if block given, define a plugin
|
152
|
+
PLUGINS[name] = block
|
153
|
+
else
|
154
|
+
# without a block execute it
|
155
|
+
blk = PLUGINS[name]
|
156
|
+
raise ArgumentError.new('Plugin :%s not defined' % name) unless blk
|
157
|
+
instance_exec &blk
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def get *args
|
162
|
+
opts.dig *args
|
163
|
+
end
|
164
|
+
|
165
|
+
# dig all options for a current class
|
166
|
+
def opts
|
167
|
+
out = {}
|
168
|
+
|
169
|
+
# dig down the ancestors tree till Object class
|
170
|
+
ancestors.each do |klass|
|
171
|
+
break if klass == Object
|
172
|
+
|
173
|
+
# copy all member and collection method options
|
174
|
+
keys = (OPTS[klass.to_s] || {}).keys
|
175
|
+
keys.each do |type|
|
176
|
+
for k, v in (OPTS.dig(klass.to_s, type) || {})
|
177
|
+
out[type] ||= {}
|
178
|
+
out[type][k] ||= v
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
out
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
# generic opts set
|
189
|
+
# set :foo, :bar, :baz
|
190
|
+
def set *args
|
191
|
+
name, value = args.pop(2)
|
192
|
+
args.unshift to_s
|
193
|
+
pointer = OPTS
|
194
|
+
|
195
|
+
for el in args
|
196
|
+
pointer[el] ||= {}
|
197
|
+
pointer = pointer[el]
|
198
|
+
end
|
199
|
+
|
200
|
+
pointer[name] = value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Joshua
|
2
|
+
module Params
|
3
|
+
class Define
|
4
|
+
def initialize
|
5
|
+
@opts = { }
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing name, *args
|
9
|
+
name = name.to_s
|
10
|
+
|
11
|
+
raise ArgumentError.new('! is not allowed in params') if name.include?('!')
|
12
|
+
|
13
|
+
type, opts = args
|
14
|
+
|
15
|
+
if type.is_a?(Hash)
|
16
|
+
opts = args.first
|
17
|
+
type = :string
|
18
|
+
end
|
19
|
+
|
20
|
+
type = :string if type.nil?
|
21
|
+
|
22
|
+
opts ||= {}
|
23
|
+
opts[:type] = type.to_s.dasherize.downcase.to_sym
|
24
|
+
|
25
|
+
opts[:required] = true if opts[:required].nil?
|
26
|
+
opts[:required] = false if name.sub! /\?$/, ''
|
27
|
+
opts[:required] = false if opts.delete(:optional)
|
28
|
+
|
29
|
+
opts.merge!(type: :boolean, default: false) if opts[:type] == :false
|
30
|
+
opts.merge!(type: :boolean, default: true) if opts[:type] == :true
|
31
|
+
opts[:required] = false if opts[:type] == :boolean
|
32
|
+
|
33
|
+
@opts[:params] ||= {}
|
34
|
+
@opts[:params][name.to_sym] = opts
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_and_clear_opts
|
38
|
+
@opts
|
39
|
+
.dup
|
40
|
+
.tap { @opts = {} }
|
41
|
+
end
|
42
|
+
|
43
|
+
def __add name, value=true
|
44
|
+
@opts[name] = value
|
45
|
+
end
|
46
|
+
|
47
|
+
def __add_annotation name, data
|
48
|
+
@opts[:annotations] ||= {}
|
49
|
+
@opts[:annotations][name] = data
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class Joshua
|
2
|
+
module Params
|
3
|
+
class Parse
|
4
|
+
class << self
|
5
|
+
def define name, &block
|
6
|
+
define_method 'check_%s' % name do |value, opts|
|
7
|
+
block.call value, opts || {}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
###
|
13
|
+
|
14
|
+
# check :boolean, 'on'
|
15
|
+
def check type, value, opts={}
|
16
|
+
opts[:required] = true if opts.delete(:req)
|
17
|
+
|
18
|
+
if value.to_s == ''
|
19
|
+
if !opts[:default].nil?
|
20
|
+
opts[:default]
|
21
|
+
elsif opts[:required]
|
22
|
+
error 'Argument missing'
|
23
|
+
end
|
24
|
+
else
|
25
|
+
m = 'check_%s' % type
|
26
|
+
hard_error 'Unsupported paramter type: %s' % type unless respond_to?(m)
|
27
|
+
|
28
|
+
if opts[:array]
|
29
|
+
delimiter = opts[:array].is_a?(TrueClass) ? /\s*[,:;]\s*/ : opts[:array]
|
30
|
+
|
31
|
+
value = value.split(delimiter) unless value.is_a?(Array)
|
32
|
+
value.map { |_| check_send m, _, opts }
|
33
|
+
else
|
34
|
+
check_send m, value, opts
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def check_send m, value, opts
|
42
|
+
send(m, value, opts).tap do |_|
|
43
|
+
error 'Value not in range of values' if opts[:values] && !opts[:values].include?(_)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def error desc
|
48
|
+
raise Joshua::Error, desc
|
49
|
+
end
|
50
|
+
|
51
|
+
def hard_error desc
|
52
|
+
raise StandardError, desc
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
class Joshua
|
4
|
+
module Params
|
5
|
+
class Parse
|
6
|
+
# params.is_active :boolean, default: false
|
7
|
+
def check_boolean value, opts={}
|
8
|
+
return false unless value
|
9
|
+
|
10
|
+
if %w(true 1 on).include?(value.to_s)
|
11
|
+
true
|
12
|
+
elsif %w(false 0 off).include?(value.to_s)
|
13
|
+
false
|
14
|
+
else
|
15
|
+
error 'Unsupported boolean param value: %s' % value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def check_integer value, opts={}
|
20
|
+
value.to_i.tap do |test|
|
21
|
+
error localized(:not_integer) if test.to_s != value.to_s
|
22
|
+
error localized(:min_value) % opts[:min] if opts[:min] && test < opts[:min]
|
23
|
+
error localized(:max_value) % opts[:max] if opts[:max] && test > opts[:max]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_string value, opts={}
|
28
|
+
value
|
29
|
+
.to_s
|
30
|
+
.sub(/^\s+/, '')
|
31
|
+
.sub(/\s+$/, '')
|
32
|
+
end
|
33
|
+
|
34
|
+
def check_float value, opts={}
|
35
|
+
value =
|
36
|
+
if opts[:round]
|
37
|
+
value.to_f.round(opts[:round])
|
38
|
+
else
|
39
|
+
value.to_f
|
40
|
+
end
|
41
|
+
|
42
|
+
error localized(:min_value) % opts[:min] if opts[:min] && value < opts[:min]
|
43
|
+
error localized(:max_value) % opts[:max] if opts[:max] && value > opts[:max]
|
44
|
+
|
45
|
+
value
|
46
|
+
end
|
47
|
+
|
48
|
+
def check_date value, opts={}
|
49
|
+
date = DateTime.parse(value)
|
50
|
+
date = DateTime.new(date.year, date.month, date.day)
|
51
|
+
|
52
|
+
check_date_min_max date, opts
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_date_time value, opts={}
|
56
|
+
date = DateTime.parse(value)
|
57
|
+
check_date_min_max date, opts
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_hash value, opts={}
|
61
|
+
value = {} unless value.is_a?(Hash)
|
62
|
+
|
63
|
+
if opts[:allow]
|
64
|
+
for key in value.keys
|
65
|
+
value.delete(key) unless opts[:allow].include?(key)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
value
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_email email, opts={}
|
73
|
+
error localized(:email_min) unless email.to_s.length > 7
|
74
|
+
error localized(:email_missing) unless email.include?('@')
|
75
|
+
email.downcase
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_url url
|
79
|
+
error localized(:url_start) unless url =~ /^https?:\/\/./
|
80
|
+
url
|
81
|
+
end
|
82
|
+
|
83
|
+
# geolocation point. google maps url will be automaticly converted
|
84
|
+
# https://www.google.com/maps/@51.5254742,-0.1057319,13z
|
85
|
+
def check_point value, opts={}
|
86
|
+
parts = value.split(/\s*,\s*/) unless parts.is_a?(Array)
|
87
|
+
|
88
|
+
error localized(:point_format) unless parts[1]
|
89
|
+
|
90
|
+
for part in parts
|
91
|
+
error localized(:point_format) unless part.include?('.')
|
92
|
+
error localized(:point_format) unless part.length > 5
|
93
|
+
end
|
94
|
+
|
95
|
+
parts.join(',')
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_oib oib, opts={}
|
99
|
+
oib = oib.to_s
|
100
|
+
|
101
|
+
return false unless oib.match(/^[0-9]{11}$/)
|
102
|
+
|
103
|
+
control_sum = (0..9).inject(10) do |middle, position|
|
104
|
+
middle += oib.at(position).to_i
|
105
|
+
middle %= 10
|
106
|
+
middle = 10 if middle == 0
|
107
|
+
middle *= 2
|
108
|
+
middle %= 11
|
109
|
+
end
|
110
|
+
|
111
|
+
control_sum = 11 - control_sum
|
112
|
+
control_sum = 0 if control_sum == 10
|
113
|
+
|
114
|
+
if control_sum == oib.at(10).to_i
|
115
|
+
oib.to_i
|
116
|
+
else
|
117
|
+
error 'Wrong OIB'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def check_date_min_max value, opts={}
|
124
|
+
if min = opts[:min]
|
125
|
+
min = DateTime.parse(min)
|
126
|
+
error localized(:min_date) % min if min > value
|
127
|
+
end
|
128
|
+
|
129
|
+
if max = opts[:max]
|
130
|
+
max = DateTime.parse(max)
|
131
|
+
error localized(:max_date) % max if value > max
|
132
|
+
end
|
133
|
+
|
134
|
+
value
|
135
|
+
end
|
136
|
+
|
137
|
+
def localized error
|
138
|
+
locale =
|
139
|
+
if defined?(Lux)
|
140
|
+
Lux.current.locale.to_s
|
141
|
+
elsif defined?(I18n)
|
142
|
+
I18n.with_locale || I18n.locale
|
143
|
+
else
|
144
|
+
:en
|
145
|
+
end
|
146
|
+
|
147
|
+
pointer = ERRORS[locale.to_sym] || ERRORS[:en]
|
148
|
+
pointer[error]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|