flex 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.
- data/LICENSE +20 -0
- data/README.md +20 -0
- data/VERSION +1 -0
- data/flex.gemspec +43 -0
- data/lib/flex.rb +418 -0
- data/lib/flex/api_methods.yml +108 -0
- data/lib/flex/class_proxy.rb +12 -0
- data/lib/flex/configuration.rb +57 -0
- data/lib/flex/errors.rb +42 -0
- data/lib/flex/http_clients/patron.rb +27 -0
- data/lib/flex/http_clients/rest_client.rb +38 -0
- data/lib/flex/loader.rb +116 -0
- data/lib/flex/logger.rb +16 -0
- data/lib/flex/model.rb +24 -0
- data/lib/flex/model/class_proxy.rb +45 -0
- data/lib/flex/model/instance_proxy.rb +101 -0
- data/lib/flex/model/manager.rb +67 -0
- data/lib/flex/rails.rb +12 -0
- data/lib/flex/rails/engine.rb +23 -0
- data/lib/flex/rails/helper.rb +16 -0
- data/lib/flex/related_model.rb +16 -0
- data/lib/flex/related_model/class_proxy.rb +23 -0
- data/lib/flex/related_model/class_sync.rb +23 -0
- data/lib/flex/related_model/instance_proxy.rb +28 -0
- data/lib/flex/result.rb +18 -0
- data/lib/flex/result/bulk.rb +20 -0
- data/lib/flex/result/collection.rb +51 -0
- data/lib/flex/result/document.rb +38 -0
- data/lib/flex/result/indifferent_access.rb +11 -0
- data/lib/flex/result/search.rb +51 -0
- data/lib/flex/result/source_document.rb +63 -0
- data/lib/flex/result/source_search.rb +32 -0
- data/lib/flex/structure/indifferent_access.rb +44 -0
- data/lib/flex/structure/mergeable.rb +21 -0
- data/lib/flex/tasks.rb +141 -0
- data/lib/flex/template.rb +187 -0
- data/lib/flex/template/base.rb +29 -0
- data/lib/flex/template/info.rb +50 -0
- data/lib/flex/template/partial.rb +31 -0
- data/lib/flex/template/search.rb +30 -0
- data/lib/flex/template/slim_search.rb +13 -0
- data/lib/flex/template/tags.rb +46 -0
- data/lib/flex/utility_methods.rb +140 -0
- data/lib/flex/utils.rb +59 -0
- data/lib/flex/variables.rb +11 -0
- data/lib/generators/flex/setup/setup_generator.rb +51 -0
- data/lib/generators/flex/setup/templates/flex_config.yml +16 -0
- data/lib/generators/flex/setup/templates/flex_dir/es.rb.erb +18 -0
- data/lib/generators/flex/setup/templates/flex_dir/es.yml.erb +19 -0
- data/lib/generators/flex/setup/templates/flex_dir/es_extender.rb.erb +17 -0
- data/lib/generators/flex/setup/templates/flex_initializer.rb.erb +44 -0
- data/lib/tasks/index.rake +23 -0
- data/test/flex.irt +143 -0
- data/test/flex/configuration.irt +53 -0
- data/test/irt_helper.rb +12 -0
- metadata +211 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Flex
|
2
|
+
class Template
|
3
|
+
module Base
|
4
|
+
|
5
|
+
def process_vars(vars)
|
6
|
+
missing = @tags - vars.keys
|
7
|
+
raise ArgumentError, "required variables #{missing.inspect} missing." \
|
8
|
+
unless missing.empty?
|
9
|
+
@partials.each do |k|
|
10
|
+
raise MissingPartialError, "undefined #{k} partial template" \
|
11
|
+
unless @host_flex.partials.has_key?(k)
|
12
|
+
next if vars[k].nil?
|
13
|
+
vars[k] = [vars[k]] unless vars[k].is_a?(Array)
|
14
|
+
vars[k] = vars[k].map {|v| @host_flex.partials[k].interpolate(@variables.deep_dup, v)}
|
15
|
+
end
|
16
|
+
vars[:index] = vars[:index].join(',') if vars[:index].is_a?(Array)
|
17
|
+
vars[:type] = vars[:type].join(',') if vars[:type].is_a?(Array)
|
18
|
+
if vars[:page]
|
19
|
+
vars[:params] ||= {}
|
20
|
+
page = vars[:page].to_i
|
21
|
+
page = 1 unless page > 0
|
22
|
+
vars[:params][:from] = ((page - 1) * vars[:params][:size] || vars[:size] || 10).ceil
|
23
|
+
end
|
24
|
+
vars
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Flex
|
2
|
+
class Template
|
3
|
+
module Info
|
4
|
+
|
5
|
+
def info(*names)
|
6
|
+
names = templates.keys if names.empty?
|
7
|
+
info = "\n"
|
8
|
+
names.each do |name|
|
9
|
+
next unless templates.include?(name)
|
10
|
+
block = ''
|
11
|
+
temp = templates[name]
|
12
|
+
meth_call = [host_class, name].join('.')
|
13
|
+
block << "########## #{meth_call} ##########\n\n#{'-' * temp.class.to_s.length}\n#{temp.class}\n#{temp.to_flex(name)}\n"
|
14
|
+
temp.partials.each do |par_name|
|
15
|
+
par = partials[par_name]
|
16
|
+
block << "#{'-' * par.class.to_s.length}\n#{par.class}\n#{par.to_flex(par_name)}\n"
|
17
|
+
end
|
18
|
+
block << "\nUsage:\n"
|
19
|
+
block << usage(meth_call, temp)
|
20
|
+
block << "\n "
|
21
|
+
info << block.split("\n").map{|l| '# ' + l}.join("\n")
|
22
|
+
info << "\ndef #{meth_call}(vars={})\n # this is a stub, used for reference\nend\n\n\n"
|
23
|
+
end
|
24
|
+
info
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def usage(meth_call, temp)
|
30
|
+
all_tags = temp.tags + temp.partials
|
31
|
+
lines = all_tags.map do |t|
|
32
|
+
comments = 'partial' if t.to_s.match(/^_/)
|
33
|
+
['', t.to_s] + (temp.variables.has_key?(t) ? ["#{temp.variables[t].inspect},", comments_to_s(comments)] : ["#{t},", comments_to_s(comments, 'required')])
|
34
|
+
end
|
35
|
+
lines.sort! { |a,b| b[3] <=> a[3] }
|
36
|
+
lines.first[0] = meth_call
|
37
|
+
lines.last[2].chop!
|
38
|
+
max = lines.transpose.map { |c| c.map(&:length).max }
|
39
|
+
lines.map { |line| "%-#{max[0]}s :%-#{max[1]}s => %-#{max[2]}s %s" % line }.join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
def comments_to_s(*comments)
|
43
|
+
comments = comments.compact
|
44
|
+
return '' if comments == []
|
45
|
+
"# #{comments.join(' ')}"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Flex
|
2
|
+
class Template
|
3
|
+
class Partial
|
4
|
+
|
5
|
+
include Base
|
6
|
+
|
7
|
+
def initialize(data, parent)
|
8
|
+
@data = data
|
9
|
+
@parent = parent
|
10
|
+
tags = Tags.new
|
11
|
+
stringified = tags.stringify(data)
|
12
|
+
@partials, @tags = tags.map(&:name).partition{|n| n.to_s =~ /^_/}
|
13
|
+
@variables = tags.variables
|
14
|
+
instance_eval <<-ruby, __FILE__, __LINE__
|
15
|
+
def interpolate(main_vars=Variables.new, vars={})
|
16
|
+
sym_vars = {}
|
17
|
+
vars.each{|k,v| sym_vars[k.to_sym] = v} # so you can pass the rails params hash
|
18
|
+
main_vars.add(@variables, sym_vars)
|
19
|
+
vars = process_vars(main_vars)
|
20
|
+
#{stringified}
|
21
|
+
end
|
22
|
+
ruby
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_flex(name=nil)
|
26
|
+
{name.to_s => @data}.to_yaml
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Flex
|
2
|
+
class Template
|
3
|
+
class Search < Template
|
4
|
+
|
5
|
+
def initialize(data, vars=nil)
|
6
|
+
super('GET', "/<<index>>/<<type>>/_search", data, vars)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_a(vars={})
|
10
|
+
int = interpolate(vars)
|
11
|
+
a = [int[:data]]
|
12
|
+
a << @instance_vars unless @instance_vars.nil?
|
13
|
+
a
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_msearch(vars={})
|
17
|
+
int = interpolate(vars, strict=true)
|
18
|
+
header = {}
|
19
|
+
header[:index] = int[:vars][:index] if int[:vars][:index]
|
20
|
+
header[:type] = int[:vars][:type] if int[:vars][:type]
|
21
|
+
[:search_type, :preferences, :routing].each do |k|
|
22
|
+
header[k] = int[:vars][k] if int[:vars][k] || int[:vars][:params] && int[:vars][:params][k]
|
23
|
+
end
|
24
|
+
data, encoded = build_data(int, vars)
|
25
|
+
"#{MultiJson.encode(header)}\n#{encoded}\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Flex
|
2
|
+
class Template
|
3
|
+
class SlimSearch < Search
|
4
|
+
|
5
|
+
# removes the fields param (no _source returned)
|
6
|
+
# the result.loaded_collection, will load the records from the db
|
7
|
+
def self.variables
|
8
|
+
super.add(:params => {:fields => ''})
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Flex
|
2
|
+
class Template
|
3
|
+
class Tags < Array
|
4
|
+
|
5
|
+
TAG_REGEXP = /<<\s*(\w+)\s*(?:=([^>]*))*>>/
|
6
|
+
|
7
|
+
def variables
|
8
|
+
tag_variables = {}
|
9
|
+
each { |t| tag_variables[t.name] = t.default if t.default || t.optional }
|
10
|
+
tag_variables
|
11
|
+
end
|
12
|
+
|
13
|
+
def stringify(structure)
|
14
|
+
structure.inspect.gsub(/(?:"#{TAG_REGEXP}"|#{TAG_REGEXP})/) do
|
15
|
+
match = $&
|
16
|
+
match =~ TAG_REGEXP
|
17
|
+
t = Tag.new($1, $2)
|
18
|
+
push t unless find{|i| i.name == t.name}
|
19
|
+
t.stringify(match !~ /^"/)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class Tag
|
26
|
+
|
27
|
+
RESERVED = [:path, :data, :params, :page, :no_pruning, :raise]
|
28
|
+
|
29
|
+
attr_reader :optional, :name, :default
|
30
|
+
|
31
|
+
def initialize(name, default)
|
32
|
+
raise SourceError, ":#{name} is a reserved symbol and cannot be used as a tag name" \
|
33
|
+
if RESERVED.include?(name)
|
34
|
+
@name = name.to_sym
|
35
|
+
@optional = !!default
|
36
|
+
@default = YAML.load(default) unless default.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def stringify(in_string)
|
40
|
+
in_string ? "\#{vars[:#{name}]}" : "vars[:#{name}]"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Flex
|
2
|
+
module UtilityMethods
|
3
|
+
|
4
|
+
def configuration
|
5
|
+
Configuration
|
6
|
+
end
|
7
|
+
alias_method :config, :configuration
|
8
|
+
|
9
|
+
# Anonymous search query: please, consider to use named templates for better performances and programming style
|
10
|
+
# data can be a JSON string that will be passed as is, or a YAML string (that will be converted into a ruby hash)
|
11
|
+
# or a hash. It can contain interpolation tags as usual.
|
12
|
+
# You can pass an optional hash of interpolation arguments (or query string :params).
|
13
|
+
# See also the Flex::Template::Search documentation
|
14
|
+
def search(data, args={})
|
15
|
+
Template::Search.new(data).render(args)
|
16
|
+
end
|
17
|
+
|
18
|
+
# like Flex.search, but it will use the Flex::Template::SlimSearch instead
|
19
|
+
def slim_search(data, args={})
|
20
|
+
Template::SlimSearch.new(data).render(args)
|
21
|
+
end
|
22
|
+
|
23
|
+
%w[HEAD GET PUT POST DELETE].each do |m|
|
24
|
+
class_eval <<-ruby, __FILE__, __LINE__
|
25
|
+
def #{m}(*args)
|
26
|
+
perform '#{m}', *args
|
27
|
+
end
|
28
|
+
ruby
|
29
|
+
end
|
30
|
+
|
31
|
+
def json2yaml(json)
|
32
|
+
YAML.dump(MultiJson.decode(json))
|
33
|
+
end
|
34
|
+
|
35
|
+
def yaml2json(yaml)
|
36
|
+
MultiJson.encode(YAML.load(yaml))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Flex.process_bulk accepts a :collection of objects, that can be hashes or Models
|
40
|
+
# you can pass also a :action set to 'index' (default) or 'delete'
|
41
|
+
# in order to bulk-index or bulk-delete the whole collection
|
42
|
+
# you can use Flex.bulk if you have an already formatted bulk data-string
|
43
|
+
def process_bulk(args)
|
44
|
+
raise ArgumentError, "Array expected as :collection (got #{args[:collection].inspect})" \
|
45
|
+
unless args[:collection].is_a?(Array)
|
46
|
+
|
47
|
+
index = args[:index] || Configuration.variables[:index]
|
48
|
+
type = args[:type] || Configuration.variables[:type]
|
49
|
+
action = args[:action] || 'index'
|
50
|
+
|
51
|
+
meta = {}
|
52
|
+
[:version, :routing, :percolate, :parent, :timestamp, :ttl].each do |opt|
|
53
|
+
meta["_#{opt}"] = args[opt] if args[opt]
|
54
|
+
end
|
55
|
+
lines = args[:collection].map do |d|
|
56
|
+
# skips indexing for objects that return nil as the indexed_json or are not flex_indexable?
|
57
|
+
unless action == 'delete'
|
58
|
+
next if d.respond_to?(:flex_indexable?) && !d.flex_flex_indexable?
|
59
|
+
json = get_json(d) || next
|
60
|
+
end
|
61
|
+
m = {}
|
62
|
+
m['_index'] = get_index(d) || index
|
63
|
+
m['_type'] = get_type(d) || type
|
64
|
+
m['_id'] = get_id(d) || d # we could pass an array of ids to delete
|
65
|
+
parent = get_parent(d)
|
66
|
+
m['_parent'] = parent if parent
|
67
|
+
routing = get_routing(d)
|
68
|
+
m['_routing'] = routing if routing
|
69
|
+
line = {action => meta.merge(m)}.to_json
|
70
|
+
line << "\n#{json}" unless action == 'delete'
|
71
|
+
line
|
72
|
+
end.compact
|
73
|
+
|
74
|
+
bulk(args.merge(:lines => lines.join("\n") + "\n")) if lines.size > 0
|
75
|
+
end
|
76
|
+
|
77
|
+
def import_collection(collection, options={})
|
78
|
+
process_bulk( {:collection => collection,
|
79
|
+
:action => 'index'}.merge(options) )
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete_collection(collection, options={})
|
84
|
+
process_bulk( {:collection => collection,
|
85
|
+
:action => 'delete'}.merge(options) )
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def perform(*args)
|
91
|
+
Template.new(*args).render
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_index(d)
|
95
|
+
d.class.flex.index if d.class.respond_to?(:flex)
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_type(d)
|
99
|
+
case
|
100
|
+
when d.respond_to?(:flex) then d.flex.type
|
101
|
+
when d.respond_to?(:_type) then d._type
|
102
|
+
when d.is_a?(Hash) then d.delete(:_type) || d.delete('_type') || d.delete(:type) || d.delete('type')
|
103
|
+
when d.respond_to?(:type) then d.type
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_parent(d)
|
108
|
+
case
|
109
|
+
when d.respond_to?(:flex) && d.flex.parent_instance(false) then d.flex.parent_instance.id
|
110
|
+
when d.respond_to?(:_parent) then d._parent
|
111
|
+
when d.respond_to?(:parent) then d.parent
|
112
|
+
when d.is_a?(Hash) then d.delete(:_parent) || d.delete('_parent') || d.delete(:parent) || d.delete('parent')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_routing(d)
|
117
|
+
case
|
118
|
+
when d.respond_to?(:flex) && d.flex.routing(false) then d.flex.routing
|
119
|
+
when d.respond_to?(:_routing) then d._routing
|
120
|
+
when d.respond_to?(:routing) then d.routing
|
121
|
+
when d.is_a?(Hash) then d.delete(:_routing) || d.delete('_routing') || d.delete(:routing) || d.delete('routing')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_id(d)
|
126
|
+
case
|
127
|
+
when d.is_a?(Hash) then d.delete(:_id) || d.delete('_id') || d.delete(:id) || d.delete('id')
|
128
|
+
when d.respond_to?(:id) then d.id
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_json(d)
|
133
|
+
case
|
134
|
+
when d.respond_to?(:flex_source) then d.flex_source
|
135
|
+
when d.respond_to?(:to_json) then d.to_json
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
data/lib/flex/utils.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Flex
|
2
|
+
module Utils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def data_from_source(source)
|
6
|
+
return unless source
|
7
|
+
data = case source
|
8
|
+
when Hash then stringified_hash(source)
|
9
|
+
when /^\s*\{.+\}\s*$/m then source
|
10
|
+
when String then YAML.load(source)
|
11
|
+
else raise ArgumentError, "expected a String or Hash instance (got #{source.inspect})"
|
12
|
+
end
|
13
|
+
raise ArgumentError, "the source does not decode to a Hash or String (got #{data.inspect})" \
|
14
|
+
unless data.is_a?(Hash) || data.is_a?(String)
|
15
|
+
data
|
16
|
+
end
|
17
|
+
|
18
|
+
def deep_merge_hashes(h1, *hashes)
|
19
|
+
merged = h1.dup
|
20
|
+
hashes.each {|h2| merged.replace(deep_merge_hash(merged,h2))}
|
21
|
+
merged
|
22
|
+
end
|
23
|
+
|
24
|
+
def erb_process(source)
|
25
|
+
ERB.new(File.read(source)).result
|
26
|
+
end
|
27
|
+
|
28
|
+
def group_array_by(ary)
|
29
|
+
h = {}
|
30
|
+
ary.each do |i|
|
31
|
+
k = yield i
|
32
|
+
if h.has_key?(k)
|
33
|
+
h[k] << i
|
34
|
+
else
|
35
|
+
h[k] = [i]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
h
|
39
|
+
end
|
40
|
+
|
41
|
+
def stringified_hash(hash)
|
42
|
+
h = {}
|
43
|
+
hash.each do |k,v|
|
44
|
+
h[k.to_s] = v.is_a?(Hash) ? stringified_hash(v) : v
|
45
|
+
end
|
46
|
+
h
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def deep_merge_hash(h1, h2)
|
52
|
+
h2 ||= {}
|
53
|
+
h1.merge(h2) do |key, oldval, newval|
|
54
|
+
oldval.is_a?(Hash) && newval.is_a?(Hash) ? deep_merge_hash(oldval, newval) : newval
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'prompter'
|
2
|
+
|
3
|
+
class Flex::SetupGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
# more funny than vanilla thor
|
6
|
+
include Prompter::Methods
|
7
|
+
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
def self.banner
|
11
|
+
"rails generate flex:setup"
|
12
|
+
end
|
13
|
+
|
14
|
+
def ask_base_name
|
15
|
+
@class_name = ask('Please, enter a class name for your Search class. Choose a name not defined in your app.',
|
16
|
+
:default => 'FlexSearch', :hint => '[<enter>=FlexSearch]')
|
17
|
+
@extender_name = "#{@class_name}Extender"
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_config_flex_file
|
21
|
+
template 'flex_config.yml', Rails.root.join('config', 'flex.yml')
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_initializer_file
|
25
|
+
template 'flex_initializer.rb.erb', Rails.root.join('config', 'initializers', 'flex.rb')
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_flex_dir
|
29
|
+
template 'flex_dir/es.rb.erb', Rails.root.join('app', 'flex', "#{@class_name.underscore}.rb")
|
30
|
+
template 'flex_dir/es.yml.erb', Rails.root.join('app', 'flex', "#{@class_name.underscore}.yml")
|
31
|
+
template 'flex_dir/es_extender.rb.erb', Rails.root.join('app', 'flex', "#{@extender_name.underscore}.rb")
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def show_setup_message
|
36
|
+
say <<-text, :style => :green
|
37
|
+
|
38
|
+
Setup done!
|
39
|
+
|
40
|
+
During prototyping, remember also:
|
41
|
+
|
42
|
+
1. each time you add a `Flex::Model` you should add its name to the "config/initializers/flex.rb"
|
43
|
+
2. each time you add/change a flex.parent relation you should reindex your DB(s) with rake `flex:import FORCE=true`
|
44
|
+
|
45
|
+
The complete documentation is available at https://github.com/ddnexus/flex/wiki
|
46
|
+
If you have any problem with Flex, please report the issue at https://github.com/ddnexus/flex/issues.
|
47
|
+
text
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|