esse 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rubocop.yml +128 -0
  4. data/CHANGELOG.md +0 -0
  5. data/Gemfile +7 -0
  6. data/Gemfile.lock +60 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +50 -0
  9. data/Rakefile +4 -0
  10. data/bin/console +22 -0
  11. data/bin/setup +8 -0
  12. data/esse.gemspec +39 -0
  13. data/exec/esse +9 -0
  14. data/lib/esse.rb +7 -0
  15. data/lib/esse/backend/index.rb +38 -0
  16. data/lib/esse/backend/index/aliases.rb +69 -0
  17. data/lib/esse/backend/index/create.rb +56 -0
  18. data/lib/esse/backend/index/delete.rb +38 -0
  19. data/lib/esse/backend/index/documents.rb +23 -0
  20. data/lib/esse/backend/index/existance.rb +23 -0
  21. data/lib/esse/backend/index/update.rb +19 -0
  22. data/lib/esse/backend/index_type.rb +32 -0
  23. data/lib/esse/backend/index_type/documents.rb +203 -0
  24. data/lib/esse/cli.rb +29 -0
  25. data/lib/esse/cli/base.rb +11 -0
  26. data/lib/esse/cli/generate.rb +51 -0
  27. data/lib/esse/cli/index.rb +14 -0
  28. data/lib/esse/cli/templates/index.rb.erb +59 -0
  29. data/lib/esse/cli/templates/mappings.json +6 -0
  30. data/lib/esse/cli/templates/serializer.rb.erb +14 -0
  31. data/lib/esse/cluster.rb +58 -0
  32. data/lib/esse/config.rb +73 -0
  33. data/lib/esse/core.rb +89 -0
  34. data/lib/esse/index.rb +21 -0
  35. data/lib/esse/index/actions.rb +10 -0
  36. data/lib/esse/index/backend.rb +13 -0
  37. data/lib/esse/index/base.rb +118 -0
  38. data/lib/esse/index/descendants.rb +17 -0
  39. data/lib/esse/index/inheritance.rb +18 -0
  40. data/lib/esse/index/mappings.rb +47 -0
  41. data/lib/esse/index/naming.rb +64 -0
  42. data/lib/esse/index/settings.rb +48 -0
  43. data/lib/esse/index/type.rb +31 -0
  44. data/lib/esse/index_mapping.rb +38 -0
  45. data/lib/esse/index_setting.rb +41 -0
  46. data/lib/esse/index_type.rb +10 -0
  47. data/lib/esse/index_type/actions.rb +11 -0
  48. data/lib/esse/index_type/backend.rb +13 -0
  49. data/lib/esse/index_type/mappings.rb +42 -0
  50. data/lib/esse/index_type/serializer.rb +87 -0
  51. data/lib/esse/primitives.rb +3 -0
  52. data/lib/esse/primitives/hstring.rb +85 -0
  53. data/lib/esse/template_loader.rb +46 -0
  54. data/lib/esse/types/mapping.rb +0 -0
  55. data/lib/esse/version.rb +5 -0
  56. metadata +215 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'ostruct'
5
+ require_relative 'base'
6
+
7
+ module Esse
8
+ module CLI
9
+ class Generate < Base
10
+ NAMESPACE_PATTERN_RE = %r{\:|/|\\}i.freeze
11
+
12
+ def self.source_root
13
+ File.dirname(__FILE__)
14
+ end
15
+
16
+ desc 'index NAME *TYPES', 'Creates a new index'
17
+ def index(name, *types)
18
+ ns_path = name.split(NAMESPACE_PATTERN_RE).tap(&:pop)
19
+ @index_name = Hstring.new(name.to_s).modulize.sub(/Index$/, '') + 'Index'
20
+ @types = types.map { |type| Hstring.new(type) }
21
+ @base_class = base_index_class(*ns_path)
22
+
23
+ base_dir = Esse.config.indices_directory.join(*ns_path)
24
+ index_name = Hstring.new(@index_name).demodulize.underscore.to_s
25
+ template(
26
+ 'templates/index.rb.erb',
27
+ base_dir.join("#{index_name}.rb"),
28
+ )
29
+ @types.each do |type|
30
+ @type = Hstring.new(type).underscore
31
+ copy_file(
32
+ 'templates/mappings.json',
33
+ base_dir.join(index_name, 'templates', "#{@type}_mapping.json"),
34
+ )
35
+ template(
36
+ 'templates/serializer.rb.erb',
37
+ base_dir.join(index_name, 'serializers', "#{@type}_serializer.rb"),
38
+ )
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def base_index_class(*ns)
45
+ return 'ApplicationIndex' if Esse.config.indices_directory.join(*ns, 'application_index.rb').exist?
46
+
47
+ 'Esse::Index'
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'base'
5
+ module Esse
6
+ module CLI
7
+ class Index < Base
8
+ desc 'create *INDEX_CLASSES', 'Creates a new index'
9
+ def create(*index_classes)
10
+ # Add action here
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @index_name %> < <%= @base_class %>
4
+ # plugin :active_record
5
+ # plugin :sequel
6
+ <%- @types.each do |type| -%>
7
+
8
+ define_type :<%= type %> do
9
+ # Collection
10
+ # ==========
11
+ #
12
+ # Collection wraps the data into an array of items that should be serialized. The first argument that is
13
+ # yielded must extends Enumerable.
14
+ # Useful for eager loading data from database or any other repository. Below is an example of a rails like
15
+ # application could load using activerecord.
16
+ #
17
+ # collection do |conditions|
18
+ # context = {}
19
+ # <%= type.camelize %>.where(conditions).find_in_batches(batch_size: 5000) do |batch|
20
+ # yield batch, context, ...
21
+ # end
22
+ # end
23
+ #
24
+ #
25
+ # Serializer
26
+ # ==========
27
+ #
28
+ # The serializer can be any class that respond with the `as_json` class method.
29
+ # And the result of its as_json is a Hash.
30
+ #
31
+ # Here is an example of a simple serializer:
32
+ # app/serializers/<%= type %>_serializer.rb
33
+ # class <%= type.camelize %>Serializer
34
+ # def initialize(<%= type %>, _context)
35
+ # @<%= type %> = <%= type %>
36
+ # end
37
+ #
38
+ # def as_json
39
+ # { '_id' => @<%= type %>.id, 'name' => @<%= type %>.name }
40
+ # end
41
+ # end
42
+ #
43
+ # And here you specify your serializer classe.
44
+ # serializer <%= @index_name %>::Serializers::<%= type.camelize %>Serializer
45
+ #
46
+ # You can also serialize the collection entry using a block:
47
+ #
48
+ # serializer(<%= type %>, context = {}) do
49
+ # hash = {
50
+ # name: <%= type %>.name,
51
+ # }
52
+ # # Context is just an example here. But it's useful for eager loading data.
53
+ # # I'll think a better example when implement this idea.
54
+ # hash[:some_attribute] = <%= type %>.some_attribute if context[:include_some_attribute]
55
+ # hash
56
+ # end
57
+ end
58
+ <%- end -%>
59
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": {
3
+ "type": "string",
4
+ "index": "analyzed"
5
+ }
6
+ }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @index_name %>::Serializers::<%= @type.camelize %>Serializer
4
+ def initialize(<%= @type %>, *_other)
5
+ @entity = <%= @type %>
6
+ end
7
+
8
+ def as_json
9
+ {
10
+ id: @entity.id, # This field is required
11
+ name: @entity.name,
12
+ }
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Cluster
5
+ ATTRIBUTES = %i[index_prefix index_settings client].freeze
6
+
7
+ # The index prefix. For example an index named UsersIndex.
8
+ # With `index_prefix = 'app1'`. Final index/alias is: 'app1_users'
9
+ attr_accessor :index_prefix
10
+
11
+ # This settings will be passed through all indices during the mapping
12
+ attr_accessor :index_settings
13
+
14
+ attr_reader :id
15
+
16
+ def initialize(id:, **options)
17
+ @id = id.to_sym
18
+ @index_settings = {}
19
+ assign(options)
20
+ end
21
+
22
+ def assign(hash)
23
+ return unless hash.is_a?(Hash)
24
+
25
+ hash.each do |key, value|
26
+ method = (ATTRIBUTES & [key.to_s, key.to_sym]).first
27
+ next unless method
28
+
29
+ public_send(:"#{method}=", value)
30
+ end
31
+ end
32
+
33
+ def client
34
+ @client ||= Elasticsearch::Client.new
35
+ end
36
+
37
+ # Define the elasticsearch client connectio
38
+ # @param client [Elasticsearch::Client, Hash] an instance of elasticsearch/api client or an hash
39
+ # with the settings that will be used to initialize Elasticsearch::Client
40
+ def client=(val)
41
+ @client = if val.is_a?(Hash)
42
+ settings = val.each_with_object({}) { |(k,v), r| r[k.to_sym] = v }
43
+ Elasticsearch::Client.new(settings)
44
+ else
45
+ val
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ attrs = ([:id] + ATTRIBUTES - [:client]).map do |method|
51
+ value = public_send(method)
52
+ format('%<k>s=%<v>p', k: method, v: value) if value
53
+ end.compact
54
+ attrs << format('client=%p', @client)
55
+ format('#<Esse::Cluster %<attrs>s>', attrs: attrs.join(' '))
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Esse
6
+ # Provides all configurations
7
+ #
8
+ # Example
9
+ # Esse.config do |conf|
10
+ # conf.indices_directory = 'app/indices'
11
+ # end
12
+ class Config
13
+ DEFAULT_CLUSTER_ID = :default
14
+ ATTRIBUTES = %i[indices_directory].freeze
15
+
16
+ # The location of the indices. Defaults to the `app/indices`
17
+ attr_reader :indices_directory
18
+
19
+ def initialize
20
+ self.indices_directory = 'app/indices'
21
+ @clusters = {}
22
+ clusters(DEFAULT_CLUSTER_ID) # initialize the :default client
23
+ end
24
+
25
+ def cluster_ids
26
+ @clusters.keys
27
+ end
28
+
29
+ def clusters(key = DEFAULT_CLUSTER_ID, **options)
30
+ return unless key
31
+
32
+ id = key.to_sym
33
+ (@clusters[id] ||= Cluster.new(id: id)).tap do |c|
34
+ c.assign(options) if options
35
+ yield c if block_given?
36
+ end
37
+ end
38
+
39
+ def indices_directory=(value)
40
+ @indices_directory = value.is_a?(Pathname) ? value : Pathname.new(value)
41
+ end
42
+
43
+ def load(arg)
44
+ case arg
45
+ when Hash
46
+ assign(arg)
47
+ when File, Pathname
48
+ # @TODO Load JSON or YAML
49
+ when String
50
+ # @TODO Load JSON or YAML if File.exist?(arg)
51
+ else
52
+ raise ArgumentError, printf('could not load configuration using: %p', val)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def assign(hash)
59
+ hash.each do |key, value|
60
+ method = (ATTRIBUTES & [key.to_s, key.to_sym]).first
61
+ next unless method
62
+
63
+ public_send("#{method}=", value)
64
+ end
65
+ if (connections = hash['clusters'] || hash[:clusters]).is_a?(Hash)
66
+ connections.each do |key, value|
67
+ clusters(key).assign(value) if value.is_a?(Hash)
68
+ end
69
+ end
70
+ true
71
+ end
72
+ end
73
+ end
data/lib/esse/core.rb ADDED
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'elasticsearch'
5
+
6
+ module Esse
7
+ @single_threaded = false
8
+ # Mutex used to protect mutable data structures
9
+ @data_mutex = Mutex.new
10
+
11
+ # Block configurations
12
+ # Esse.config do |conf|
13
+ # conf.indices_directory = 'app/indices/directory'
14
+ # conf.clusters(:v1) do |cluster|
15
+ # cluster.index_prefix = 'backend'
16
+ # cluster.client = Elasticsearch::Client.new
17
+ # cluster.index_settings = {
18
+ # number_of_shards: 2,
19
+ # number_of_replicas: 0
20
+ # }
21
+ # end
22
+ # end
23
+ #
24
+ # Inline configurations
25
+ # Esse.config.indices_directory = 'app/indices/directory'
26
+ # Esse.config.clusters(:v1).client = Elasticsearch::Client.new
27
+ def self.config
28
+ @config ||= Config.new
29
+ yield(@config) if block_given?
30
+ @config
31
+ end
32
+
33
+ # Unless in single threaded mode, protects access to any mutable
34
+ # global data structure in Esse.
35
+ # Uses a non-reentrant mutex, so calling code should be careful.
36
+ # In general, this should only be used around the minimal possible code
37
+ # such as Hash#[], Hash#[]=, Hash#delete, Array#<<, and Array#delete.
38
+ def self.synchronize(&block)
39
+ @single_threaded ? yield : @data_mutex.synchronize(&block)
40
+ end
41
+
42
+ # Generates an unique timestamp to be used as a index suffix.
43
+ # Time.now.to_i could also do the job. But I think this format
44
+ # is more readable for humans
45
+ def self.timestamp
46
+ Time.now.strftime('%Y%m%d%H%M%S')
47
+ end
48
+
49
+ # Simple helper used to fetch Hash value using Symbol and String keys.
50
+ #
51
+ # @param [Hash] the JSON document
52
+ # @option [Array] :delete Removes the hash key and return its value
53
+ # @option [Array] :keep Fetch the hash key and return its value
54
+ # @return [Array([Integer, String, nil], Hash)] return the key value and the modified hash
55
+ def self.doc_id!(hash, delete: %w[_id], keep: %w[id])
56
+ return unless hash.is_a?(Hash)
57
+
58
+ id = nil
59
+ modified = nil
60
+ Array(delete).each do |key|
61
+ k = key.to_s if hash.key?(key.to_s)
62
+ k ||= key.to_sym if hash.key?(key.to_sym)
63
+ next unless k
64
+
65
+ modified ||= hash.dup
66
+ id = modified.delete(k)
67
+ break if id
68
+ end
69
+ return [id, modified] if id
70
+
71
+ modified ||= hash
72
+ Array(keep).each do |key|
73
+ id = modified[key.to_s] || modified[key.to_sym]
74
+ break if id
75
+ end
76
+ [id, modified]
77
+ end
78
+
79
+ require_relative 'config'
80
+ require_relative 'cluster'
81
+ require_relative 'primitives'
82
+ require_relative 'index_type'
83
+ require_relative 'index_setting'
84
+ require_relative 'index_mapping'
85
+ require_relative 'template_loader'
86
+ require_relative 'backend/index'
87
+ require_relative 'backend/index_type'
88
+ require_relative 'version'
89
+ end
data/lib/esse/index.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core'
4
+
5
+ module Esse
6
+ class Index
7
+ require_relative 'index/base'
8
+ require_relative 'index/inheritance'
9
+ require_relative 'index/actions'
10
+ require_relative 'index/naming'
11
+ require_relative 'index/type'
12
+ require_relative 'index/settings'
13
+ require_relative 'index/mappings'
14
+ require_relative 'index/descendants'
15
+ require_relative 'index/backend'
16
+
17
+ @cluster_id = nil
18
+
19
+ def_Index(::Esse)
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ end
7
+
8
+ extend ClassMethods
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ def backend
7
+ Esse::Backend::Index.new(self)
8
+ end
9
+ end
10
+
11
+ extend ClassMethods
12
+ end
13
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ attr_reader :cluster_id
7
+
8
+ # Define a Index method on the given module that calls the Index
9
+ # method on the receiver. This is how the Esse::Index() method is
10
+ # defined, and allows you to define Index() methods on other modules,
11
+ # making it easier to have custom index settings for all indexes under
12
+ # a namespace. Example:
13
+ #
14
+ # module V1
15
+ # EsIndex = Class.new(Esse::Index)
16
+ # EsIndex.def_Index(self)
17
+ #
18
+ # class Bar < EsIndex
19
+ # # Uses :default elasticsearch client connection
20
+ # end
21
+ #
22
+ # class Baz < EsIndex(:v1)
23
+ # # Uses :v1 elasticsearch client connection
24
+ # end
25
+ # end
26
+ def def_Index(index_module) # rubocop:disable Naming/MethodName
27
+ tap do |model|
28
+ index_module.define_singleton_method(:Index) do |source|
29
+ model.Index(source)
30
+ end
31
+ end
32
+ end
33
+
34
+ # Lets you create a Index subclass with its elasticsearch cluster
35
+ #
36
+ # Example:
37
+ # # Using a custom cluster
38
+ # Esse.config.clusters(:v1).client = Elasticsearch::Client.new
39
+ # class UsersIndex < Esse::Index(:v1)
40
+ # end
41
+ #
42
+ # # Using :default cluster
43
+ # class UsersIndex < Esse::Index
44
+ # end
45
+ def Index(source) # rubocop:disable Naming/MethodName
46
+ klass = Class.new(self)
47
+
48
+ valid_ids = Esse.config.cluster_ids
49
+ klass.cluster_id = \
50
+ case source
51
+ when Esse::Cluster
52
+ source.id
53
+ when String, Symbol
54
+ id = source.to_sym
55
+ id if valid_ids.include?(id)
56
+ end
57
+
58
+ msg = <<~MSG
59
+ We could not resolve the index cluster using the argument %<arg>p. \n
60
+ It must be previously defined in the `Esse.config' settings. \n
61
+ Here is the list of cluster ids we have configured: %<ids>s\n
62
+
63
+ You can ignore this cluster id entirely. That way the :default id will be used.\n
64
+ Example: \n
65
+ class UsersIndex < Esse::Index\n
66
+ end\n
67
+ MSG
68
+ unless klass.cluster_id
69
+ raise ArgumentError.new, format(msg, arg: source, ids: valid_ids.map(&:inspect).join(', '))
70
+ end
71
+
72
+ klass.type_hash = {}
73
+ klass
74
+ end
75
+
76
+ # Sets the client_id associated with the Index class.
77
+ # This can be used directly on Esse::Index to set the :default es cluster
78
+ # to be used by subclasses, or to override the es client used for specific indices:
79
+ # Esse::Index.cluster_id = :v1
80
+ # ArtistIndex = Class.new(Esse::Index)
81
+ # ArtistIndex.cluster_id = :v2
82
+ def cluster_id=(cluster_id)
83
+ @cluster_id = cluster_id
84
+ end
85
+
86
+ # @return [Symbol] reads the @cluster_id instance variable or :default
87
+ def cluster_id
88
+ @cluster_id || Config::DEFAULT_CLUSTER_ID
89
+ end
90
+
91
+ # @return [Esse::Cluster] an instance of cluster based on its cluster_id
92
+ def cluster
93
+ unless Esse.config.cluster_ids.include?(cluster_id)
94
+ raise NotImplementedError, <<~MSG
95
+ There is no cluster configured for this index. Use `Esse.config.clusters(cluster_id) { ... }' define the elasticsearch
96
+ client connection.
97
+ MSG
98
+ end
99
+
100
+ Esse.synchronize { Esse.config.clusters(cluster_id) }
101
+ end
102
+
103
+ def inspect
104
+ if self == Index
105
+ super
106
+ elsif abstract_class?
107
+ "#{super}(abstract)"
108
+ elsif index_name?
109
+ "#{super}(Index: #{index_name})"
110
+ else
111
+ "#{super}(Index is not defined)"
112
+ end
113
+ end
114
+ end
115
+
116
+ extend ClassMethods
117
+ end
118
+ end