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.
@@ -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
@@ -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