leafy-ruby 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0cf163d4cd002f7035394d9997d425b7f0834fc2cc91a42a516f3de9de5c6bfe
4
+ data.tar.gz: 234f95015a72ad3aacf0db48bfe6c4bc5f7c48db0f1e474c6a88516b9e0a3911
5
+ SHA512:
6
+ metadata.gz: c4c5342b0a90c406bd6cb4e04969f3e9ec59cb10e03d91e2ea1f91d2db8f6cc125809ee23bade0b098480db82a22cba079d344ff4a599b9db9085cd0ee55b8a4
7
+ data.tar.gz: 8d6cb3f31e9bab0543e51a3b5344ef6ddd26c1052ad07254b6ef89365fd5ab9a55d92de8472688034da3a51191601ac0924eda53b9bdca497d9890c1e48b482d
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea/*
10
+ /coverage/*
11
+ .rbenv-gemsets
12
+ Gemfile.lock
13
+ # rspec failure tracking
14
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,38 @@
1
+ sudo: false
2
+
3
+ language: ruby
4
+
5
+ rvm:
6
+ - 2.2
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
10
+ - ruby-head
11
+ - jruby-9.2.0.0
12
+ - jruby-head
13
+
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: ruby-head
17
+ - rvm: jruby-head
18
+
19
+ before_install: gem install bundler
20
+
21
+ before_script:
22
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
23
+ - chmod +x ./cc-test-reporter
24
+ - ./cc-test-reporter before-build
25
+
26
+ script: bundle exec rspec
27
+
28
+ after_script:
29
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
30
+
31
+ cache: bundler
32
+
33
+ env:
34
+ global:
35
+ CC_TEST_REPORTER_ID=5c94ea74238649cee4b51abbe647de62a3d63971eaeca9aa58c82b62d9a0e6a8
36
+ COVERAGE=1
37
+
38
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in leafy.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Evgeny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Leafy [![Build Status](https://travis-ci.org/estepnv/leafy.svg?branch=master)](https://travis-ci.org/estepnv/leafy) [![Maintainability](https://api.codeclimate.com/v1/badges/5108d8a1ac5e2915f30f/maintainability)](https://codeclimate.com/github/estepnv/leafy/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/5108d8a1ac5e2915f30f/test_coverage)](https://codeclimate.com/github/estepnv/leafy/test_coverage)
2
+
3
+ A toolkit for dynamic custom attributes for Ruby applications.
4
+
5
+ * Simple modular design - load only things you need
6
+ * Stored as JSON with your models - allows you to avoid expensive JOIN queries (supports postgresql json/jsonb data types)
7
+ * Type inference - Infers type from custom field data
8
+ * Add your own custom field types
9
+
10
+ Supported data types:
11
+ - `string` - strings
12
+ - `integer` - integer numbers
13
+ - `double` - floating point numbers
14
+ - `datetime` - `Time` instances
15
+ - `date` - `Date` instances
16
+ - `bool` - `TrueClass` and `FalseClass` instances
17
+
18
+ ## Quick start
19
+
20
+ Add Leafy to Gemfile
21
+
22
+ ```ruby
23
+ gem 'leafy-ruby'
24
+ ```
25
+
26
+ **Plain Ruby app**
27
+
28
+ Include "Plain old ruby object" mixin into your class definition to start using leafy
29
+
30
+ ```ruby
31
+ class SchemaHost < ActiveRecord::Base
32
+ include Leafy::Mixin::Schema[:poro]
33
+
34
+ attr_accessor :leafy_data
35
+ end
36
+
37
+ class FieldsHost < ActiveRecord::Base
38
+ include Leafy::Mixin::Fields[:poro]
39
+
40
+ attr_accessor :leafy_data
41
+ attr_accessor :leafy_fields
42
+ end
43
+ ```
44
+
45
+ Schema mixin introduces next methods:
46
+
47
+ - `#leafy_fields (Schema)` returns Schema instance allowing you to iterate through custom attribute definitions.
48
+ - `#leafy_fields=` schema setter method
49
+ - `#leafy_fields_attributes=` nested attributes setter method
50
+
51
+ Fields mixin:
52
+
53
+ - `#leafy_values (Hash)` returns a hash representation of your fields data
54
+ - `#leafy_values=` allows you to assign custom attributes data
55
+ - `#leafy_fields_values (Leafy::FieldValueCollection)` returns a collection of `Field::Value` instances which provide more control over values data
56
+
57
+ **Please note**:
58
+ Leafy is stateless and changing Schema instance won't reflect on your active record model instance.
59
+ For changes to take place you have to explicitly assign schema or attributes data to the model.
60
+
61
+
62
+
63
+ ```ruby
64
+ host = SchemaHost.new
65
+ host.leafy_fields_attributes = [
66
+ { name: "Field 1", type: :integer, id: "id_1", metadata: { default: 1, placeholder: "enter an integer", required: true } },
67
+ { name: "Field 2", type: :string, id: "id_2", metadata: { default: "", placeholder: "enter value" } },
68
+ { name: "Field 3", type: :datetime, id: "id_3", metadata: { order: 10000 } }
69
+ ]
70
+
71
+ # or build schema yourself
72
+
73
+ field_1 = Leafy::Field.new(name: "Field 1", type: :integer, id: "id_1", default: 1, placeholder: "enter an integer", required: true)
74
+ field_2 = Leafy::Field.new(name: "Field 2", type: :string, id: "id_2", default: "", placeholder: "enter value")
75
+ field_3 = Leafy::Field.new(name: "Field 3", type: :datetime, id: "id_3", order: 10000)
76
+
77
+ schema = Leafy::Schema.new
78
+ schema << field_1
79
+ schema << field_2
80
+ schema << field_3
81
+
82
+ host.leafy_fields = schema
83
+
84
+ # after that reference schema for fields target instance
85
+
86
+ target = FieldsHost.new
87
+ target.leafy_fields = host.leafy_fields
88
+ target.leafy_values
89
+
90
+ # => { "id_1" => nil, "id_2" => nil, "id_3" => nil }
91
+
92
+ target.leafy_values = { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00"), "junk": "some junk data" }
93
+ target.leafy_values
94
+
95
+ # => { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00") }
96
+ ```
97
+
98
+ **ActiveRecord**
99
+
100
+ Add migration
101
+ ```ruby
102
+ add_column :schema_hosts, :leafy_data, :text, null: false, default: "{}"
103
+ add_column :fields_hosts, :leafy_data, :text, null: false, default: "{}"
104
+ # for postgresql
105
+ # add_column :leafy_data, :jsonb, null: false, default: {}
106
+ ```
107
+
108
+ Update your models
109
+
110
+ ```ruby
111
+ class SchemaHost < ActiveRecord::Base
112
+ include Leafy::Mixin::Schema[:active_record]
113
+ end
114
+
115
+ class FieldsHost < ActiveRecord::Base
116
+ include Leafy::Mixin::Fields[:active_record]
117
+
118
+ belongs_to :schema_host, required: true
119
+ delegate :leafy_fields, to: :schema_host
120
+ end
121
+ ```
122
+
123
+ ```ruby
124
+ host = SchemaHost.create(
125
+ leafy_fields_attributes: [
126
+ { name: "Field 1", type: :integer, id: "id_1", metadata: { default: 1, placeholder: "enter an integer", required: true } },
127
+ { name: "Field 2", type: :string, id: "id_2", metadata: { default: "", placeholder: "enter value" } },
128
+ { name: "Field 3", type: :datetime, id: "id_3", metadata: { order: 10000 } }
129
+ ]
130
+ )
131
+
132
+ target = FieldsHost.create(schema_host: host)
133
+ target.leafy_values
134
+
135
+ # => { "id_1" => nil, "id_2" => nil, "id_3" => nil }
136
+
137
+ target.leafy_values = { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00"), "junk": "some junk data" }
138
+ target.save!
139
+ target.reload
140
+
141
+ target.leafy_values
142
+
143
+ # => { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00") }
144
+ ```
145
+
146
+ ## Adding your own types
147
+
148
+ Leafy allows adding your own data types
149
+ To allow leafy process your own data type you need to describe how to store it. For that purpose leafy utilizes converter classes associated for each type.
150
+
151
+ Converter instance has to implement `#dump` and `#load` methods
152
+
153
+ ```ruby
154
+ class MyComplexTypeConverter
155
+ def self.load(json_string)
156
+ # parsing logic
157
+ return MyComplexType.new(parsed_data)
158
+ end
159
+
160
+ def self.dump(my_complex_type_instance)
161
+ # serializing logic
162
+ return json
163
+ end
164
+ end
165
+
166
+ Leafy.register_converter(:complex_type, MyComplexTypeConverter)
167
+ ```
168
+
169
+
170
+
171
+ ## Contributing
172
+
173
+ Bug reports and pull requests are welcome on GitHub at https://github.com/estepnv/leafy.
174
+
175
+ ## License
176
+
177
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "leafy"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/leafy.gemspec ADDED
@@ -0,0 +1,46 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "leafy/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "leafy-ruby"
8
+ spec.version = Leafy.version
9
+ spec.authors = ["Evgeny Stepanov"]
10
+ spec.email = ["estepnv@icloud.com"]
11
+
12
+ spec.summary = %q{Toolkit for custom attributes in Ruby apps}
13
+ spec.description = <<-DESC
14
+ Leafy is toolkit that allows you to integrate dynamic custom attributes functionality into your ruby application.
15
+
16
+ It provides high level API you can use for any suitable use-case.
17
+ It ships with several basic data types and allows you to add your own data type converters.
18
+ DESC
19
+
20
+ spec.homepage = "https://gibhub.com/estepnv/leafy"
21
+ spec.license = "MIT"
22
+
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.16"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "simplecov"
34
+
35
+ if RUBY_VERSION >= "2.2.2"
36
+ spec.add_development_dependency "activerecord", "~> 5.0"
37
+ else
38
+ spec.add_development_dependency "activerecord", "~> 4.2"
39
+ end
40
+
41
+ if RUBY_ENGINE == "jruby"
42
+ spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "51"
43
+ else
44
+ spec.add_development_dependency "sqlite3"
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class BoolConverter
6
+ def dump(value)
7
+ target = value
8
+ target = load(target) unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
9
+
10
+ target ? "1" : "0"
11
+ end
12
+
13
+ def load(value)
14
+ return if value.nil?
15
+
16
+ target = value.respond_to?(:downcase) ? (value.downcase rescue nil) : value
17
+ return true if ["1", "true", "t", 1, "yes", "y", true].include?(target)
18
+ return false if ["0", "false", "f", 0, "no", "n", false].include?(target)
19
+
20
+ raise(ArgumentError, "can't parse value to bool: #{value}")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Leafy
6
+ module Converter
7
+ class DateConverter
8
+
9
+ def dump(value)
10
+ return if value.nil?
11
+
12
+ target = value.dup
13
+ target = load(target) if target.is_a?(String)
14
+ target = target.dup.to_date if target.is_a?(Time)
15
+
16
+ unless target.is_a?(Date)
17
+ raise(ArgumentError, "is not a Date object")
18
+ end
19
+
20
+ target.iso8601
21
+ end
22
+
23
+ def load(value)
24
+ return if value.nil?
25
+ return value if value.is_a?(Date)
26
+ Date.parse(value)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Leafy
6
+ module Converter
7
+ class DatetimeConverter
8
+
9
+ def dump(value)
10
+ return if value.nil?
11
+
12
+ target = value.dup
13
+ target = load(target) if target.is_a?(String)
14
+
15
+ raise(ArgumentError, "is not a Time object") unless target.is_a?(Time)
16
+
17
+ target.utc.iso8601
18
+ end
19
+
20
+ def load(value)
21
+ return if value.nil?
22
+ return value if value.is_a?(Time)
23
+ Time.parse(value).utc
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class DoubleConverter
6
+ def dump(value)
7
+ return if value.nil?
8
+ value.to_s
9
+ end
10
+
11
+ def load(value)
12
+ return if value.nil?
13
+ value.to_f
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class DummyConverter
6
+ def dump(value)
7
+ value
8
+ end
9
+
10
+ def load(value)
11
+ value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class IntegerConverter
6
+ def dump(value)
7
+ value.nil? ? nil : value.to_s
8
+ end
9
+
10
+ def load(value)
11
+ value.nil? ? nil : value.to_i
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Converter
5
+ class StringConverter
6
+ def dump(value)
7
+ value
8
+ end
9
+
10
+ def load(value)
11
+ value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+
4
+ module Leafy
5
+ class Field
6
+ attr_accessor :id, :name, :type, :metadata
7
+
8
+ def initialize(attributes = {})
9
+ raise ArgumentError, "attributes is not a Hash" unless attributes.is_a?(Hash)
10
+ attributes = Leafy::Utils.symbolize_keys(attributes)
11
+
12
+ self.name = attributes[:name]
13
+ self.type = attributes[:type]
14
+ self.id = attributes.fetch(:id) { [name.downcase.strip.tr(" ", "-"), SecureRandom.uuid].join("-") }
15
+ self.metadata = attributes.fetch(:metadata, {})
16
+ end
17
+
18
+ def serializable_hash
19
+ {
20
+ name: name,
21
+ type: type,
22
+ id: id,
23
+ metadata: metadata
24
+ }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ class FieldValue
5
+ attr_accessor :id, :name, :type, :raw, :converter
6
+
7
+ def initialize(attributes)
8
+ attributes = attributes.dup.to_a.map { |pair| [pair[0].to_sym, pair[1]]}.to_h
9
+
10
+ self.id = attributes.fetch(:id)
11
+ self.name = attributes.fetch(:name)
12
+ self.type = attributes.fetch(:type)
13
+ self.converter = attributes.fetch(:converter) { Leafy.converters.fetch(type.to_sym) { raise(RuntimeError, "unregistered converter #{type}") } }
14
+ self.raw = attributes.fetch(:raw)
15
+ end
16
+
17
+ def value
18
+ converter.load(raw)
19
+ end
20
+
21
+ def value=(val)
22
+ self.raw = converter.dump(converter.load(val))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ class FieldValueCollection
5
+ include ::Enumerable
6
+
7
+ def each
8
+ if block_given?
9
+ @leafy_field_values.each { |i| yield i }
10
+ else
11
+ @leafy_field_values.each
12
+ end
13
+ end
14
+
15
+ def [](index)
16
+ to_a[index]
17
+ end
18
+
19
+ def size
20
+ count
21
+ end
22
+
23
+ def initialize(leafy_fields, values = {})
24
+ @leafy_fields = leafy_fields
25
+ @leafy_field_values = leafy_fields.map do |custom_field|
26
+ Leafy::FieldValue.new(
27
+ id: custom_field.id,
28
+ name: custom_field.name,
29
+ raw: values[custom_field.id],
30
+ type: custom_field.type
31
+ )
32
+ end
33
+ end
34
+
35
+ def values
36
+ inject({}) do |acc, field_value|
37
+ acc[field_value.id] = field_value.value
38
+ acc
39
+ end
40
+ end
41
+
42
+ def values=(attributes = {})
43
+ attributes = attributes.dup.to_a.map { |pair| [pair[0].to_s, pair[1]]}.to_h
44
+
45
+ @leafy_field_values.each do |field_value|
46
+ field_value.value = attributes[field_value.id]
47
+ end
48
+ end
49
+
50
+ def self.dump(field_values_collection)
51
+ JSON.dump(field_values_collection.map { |field_value| [field_value.id, field_value.raw] }.to_h)
52
+ end
53
+
54
+ def self.load(leafy_fields, data)
55
+ Leafy::FieldValueCollection.new(leafy_fields, JSON.load(data))
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Leafy
6
+ module Mixin
7
+ module ActiveRecord
8
+
9
+ module Fields
10
+ module ClassMethods
11
+ end
12
+
13
+ module InstanceMethods
14
+
15
+ def leafy_fields
16
+ raise(RuntimeError, "Leafy: leafy_fields method is not defined")
17
+ end
18
+
19
+ def leafy_values
20
+ leafy_field_values.values
21
+ end
22
+
23
+ def leafy_values=(attributes = {})
24
+ field_value_list = leafy_field_values
25
+ field_value_list.values = attributes
26
+
27
+ self.leafy_data = ::Leafy::FieldValueCollection.dump(field_value_list)
28
+ end
29
+
30
+ def leafy_field_values
31
+ ::Leafy::FieldValueCollection.load(leafy_fields, leafy_data || "{}")
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Leafy
6
+ module Mixin
7
+ module ActiveRecord
8
+
9
+ module Schema
10
+ module InstanceMethods
11
+ def leafy_fields
12
+ data = _leafy_data
13
+
14
+ activerecord_json_column? ?
15
+ ::Leafy::Schema.new(data) :
16
+ ::Leafy::Schema.load(data.nil? ? "[]" : data)
17
+ end
18
+
19
+ def leafy_fields=(leafy_schema)
20
+ self._leafy_data = activerecord_json_column? ?
21
+ leafy_schema.serializable_hash :
22
+ ::Leafy::Schema.dump(leafy_schema)
23
+ end
24
+
25
+ def leafy_fields_attributes=(attributes_list)
26
+ self.leafy_fields = ::Leafy::Schema.new(attributes_list)
27
+ end
28
+
29
+ end
30
+
31
+ module ClassMethods
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Leafy
6
+ module Mixin
7
+ module ActiveRecord
8
+ module Shared
9
+
10
+ module InstanceMethods
11
+ private
12
+
13
+ def activerecord_json_column?
14
+ return false unless self.is_a?(::ActiveRecord::Base)
15
+ return false unless column = self.class.columns_hash[self.class.leafy_data_attribute.to_s]
16
+
17
+ [:json, :jsonb].include?(column.type)
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ module Leafy
2
+ module Mixin
3
+ module DataAccessor
4
+
5
+ module ClassMethods
6
+ def leafy_data_attribute=(value)
7
+ @leafy_data_attribute = value
8
+ end
9
+
10
+ def leafy_data_attribute
11
+ @leafy_data_attribute ||= :leafy_data
12
+ end
13
+
14
+ def leafy_data_getter; leafy_data_attribute; end
15
+
16
+ def leafy_data_setter; "#{leafy_data_attribute}="; end
17
+ end
18
+
19
+ module InstanceMethods
20
+ private
21
+
22
+ def _leafy_data=(value)
23
+ unless respond_to?(self.class.leafy_data_setter)
24
+ raise(RuntimeError, "must respond to ##{self.class.leafy_data_setter}") unless respond_to?(:leafy_data=)
25
+ end
26
+
27
+ public_send(self.class.leafy_data_setter, value)
28
+ end
29
+
30
+ def _leafy_data
31
+ unless respond_to?(self.class.leafy_data_getter)
32
+ raise(RuntimeError, "must respond to ##{self.class.leafy_data_getter}")
33
+ end
34
+
35
+ public_send(self.class.leafy_data_getter)
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./data_accessor"
4
+
5
+ module Leafy
6
+ module Mixin
7
+ module Fields
8
+
9
+ def self.[](orm = :poro)
10
+ Module.new do
11
+ @orm = orm
12
+
13
+ def self.included(base)
14
+
15
+ base.extend DataAccessor::ClassMethods
16
+ base.include DataAccessor::InstanceMethods
17
+
18
+ case @orm
19
+
20
+ when :poro
21
+
22
+ require_relative "./poro/fields"
23
+
24
+ base.extend Poro::Fields::ClassMethods
25
+ base.include Poro::Fields::InstanceMethods
26
+
27
+ when :active_record
28
+
29
+ require_relative "./active_record/shared"
30
+ require_relative "./active_record/fields"
31
+
32
+ base.extend ActiveRecord::Fields::ClassMethods
33
+ base.include ActiveRecord::Shared::InstanceMethods
34
+ base.include ActiveRecord::Fields::InstanceMethods
35
+
36
+ else
37
+
38
+ raise(RuntimeError, "Leafy: unsupported schema storage: #{orm}")
39
+
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Mixin
5
+ module Poro
6
+
7
+ module Fields
8
+ module ClassMethods
9
+ end
10
+
11
+ module InstanceMethods
12
+
13
+ def leafy_fields
14
+ raise(RuntimeError, "Leafy: leafy_fields method is not defined")
15
+ end
16
+
17
+ def leafy_values
18
+ leafy_field_values.values
19
+ end
20
+
21
+ def leafy_values=(attributes = {})
22
+ field_value_list = leafy_field_values
23
+ field_value_list.values = attributes
24
+
25
+ self._leafy_data = ::Leafy::FieldValueCollection.dump(field_value_list)
26
+ end
27
+
28
+ def leafy_field_values
29
+ ::Leafy::FieldValueCollection.load(leafy_fields, _leafy_data || "{}")
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ module Mixin
5
+ module Poro
6
+
7
+ module Schema
8
+
9
+ module InstanceMethods
10
+ def leafy_fields
11
+ data = _leafy_data
12
+ Leafy::Schema.load(data.nil? ? "[]" : data)
13
+ end
14
+
15
+ def leafy_fields=(leafy_schema)
16
+ self._leafy_data = ::Leafy::Schema.dump(leafy_schema)
17
+ end
18
+
19
+ def leafy_fields_attributes=(attributes_list)
20
+ self.leafy_fields = ::Leafy::Schema.new(attributes_list)
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./data_accessor"
4
+
5
+ module Leafy
6
+ module Mixin
7
+ module Schema
8
+
9
+ def self.[](orm)
10
+ Module.new do
11
+ @orm = orm
12
+
13
+ def self.included(base)
14
+
15
+ base.extend DataAccessor::ClassMethods
16
+ base.include DataAccessor::InstanceMethods
17
+
18
+ case @orm
19
+ when :poro
20
+
21
+ require_relative "./poro/schema"
22
+
23
+ base.extend Poro::Schema::ClassMethods
24
+ base.include Poro::Schema::InstanceMethods
25
+
26
+ when :active_record
27
+
28
+ require_relative "./active_record/shared"
29
+ require_relative "./active_record/schema"
30
+
31
+ base.extend ActiveRecord::Schema::ClassMethods
32
+ base.include ActiveRecord::Shared::InstanceMethods
33
+ base.include ActiveRecord::Schema::InstanceMethods
34
+
35
+ else
36
+
37
+ raise(RuntimeError, "Leafy: unsupported schema storage: #{orm}")
38
+
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+
4
+ module Leafy
5
+ class Schema
6
+ include ::Enumerable
7
+
8
+ def each
9
+ if block_given?
10
+ @fields.each { |i| yield i }
11
+ else
12
+ @fields.each
13
+ end
14
+ end
15
+
16
+ def initialize(fields = [])
17
+ @fields = fields.map { |field| Leafy::Field.new(field) }
18
+ end
19
+
20
+ def ids
21
+ @fields.map(&:id)
22
+ end
23
+
24
+ def [](identifier)
25
+ @fields.find { |f| f.id == identifier }
26
+ end
27
+
28
+ def push(field)
29
+ raise(ArgumentError, "is not a field") unless field.is_a?(Leafy::Field)
30
+ @fields << field
31
+ end
32
+
33
+ def serializable_hash
34
+ @fields.map(&:serializable_hash)
35
+ end
36
+
37
+ def self.dump(schema)
38
+ JSON.dump(schema.serializable_hash)
39
+ end
40
+
41
+ def self.load(data)
42
+ Schema.new(JSON.parse(data))
43
+ end
44
+
45
+ alias :<< :push
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ module Leafy
2
+ module Utils
3
+ def self.symbolize_keys(hash)
4
+ hash.dup.to_a.map do |pair|
5
+ key, value = pair
6
+ [key.to_sym, value.is_a?(Hash) ? Leafy::Utils.symbolize_keys(value) : value]
7
+ end.to_h
8
+ end
9
+
10
+ def self.stringify_keys(hash)
11
+ hash.dup.to_a.map do |pair|
12
+ key, value = pair
13
+ [key.to_s, value.is_a?(Hash) ? Leafy::Utils.stringify_keys(value) : value]
14
+ end.to_h
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leafy
4
+ def self.version
5
+ Gem::Version.new VERSION::STRING
6
+ end
7
+
8
+ module VERSION
9
+ MAJOR = 0
10
+ MINOR = 0
11
+ TINY = 1
12
+ PRE = nil
13
+
14
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15
+ end
16
+ end
data/lib/leafy.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "leafy/version"
4
+ require "leafy/utils"
5
+ require "leafy/field"
6
+ require "leafy/schema"
7
+ require "leafy/field_value"
8
+ require "leafy/field_value_collection"
9
+ Dir[File.expand_path("../leafy/converter/**/*.rb", __FILE__)].each { |f| require f }
10
+ require "leafy/mixin/schema"
11
+ require "leafy/mixin/fields"
12
+
13
+
14
+ # module definition
15
+ module Leafy
16
+ def self.register_converter(name, converter)
17
+ raise(ArgumentError, "converter is not provided") if converter.nil?
18
+
19
+ if !converter.respond_to?(:load) || !converter.respond_to?(:dump)
20
+ raise(ArgumentError, "converter must respond to #dump and #load")
21
+ end
22
+
23
+ converters[name] = converter
24
+ end
25
+
26
+ def self.converters
27
+ @converters ||= {}
28
+ end
29
+ end
30
+
31
+ Leafy.register_converter(:dummy, Leafy::Converter::DummyConverter.new)
32
+ Leafy.register_converter(:string, Leafy::Converter::StringConverter.new)
33
+ Leafy.register_converter(:integer, Leafy::Converter::IntegerConverter.new)
34
+ Leafy.register_converter(:double, Leafy::Converter::DoubleConverter.new)
35
+ Leafy.register_converter(:datetime, Leafy::Converter::DatetimeConverter.new)
36
+ Leafy.register_converter(:bool, Leafy::Converter::BoolConverter.new)
37
+ Leafy.register_converter(:date, Leafy::Converter::DateConverter.new)
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: leafy-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Evgeny Stepanov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activerecord
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: |2
98
+ Leafy is toolkit that allows you to integrate dynamic custom attributes functionality into your ruby application.
99
+
100
+ It provides high level API you can use for any suitable use-case.
101
+ It ships with several basic data types and allows you to add your own data type converters.
102
+ email:
103
+ - estepnv@icloud.com
104
+ executables: []
105
+ extensions: []
106
+ extra_rdoc_files: []
107
+ files:
108
+ - ".gitignore"
109
+ - ".rspec"
110
+ - ".travis.yml"
111
+ - Gemfile
112
+ - LICENSE.txt
113
+ - README.md
114
+ - Rakefile
115
+ - bin/console
116
+ - bin/setup
117
+ - leafy.gemspec
118
+ - lib/leafy.rb
119
+ - lib/leafy/converter/bool_converter.rb
120
+ - lib/leafy/converter/date_converter.rb
121
+ - lib/leafy/converter/datetime_converter.rb
122
+ - lib/leafy/converter/double_converter.rb
123
+ - lib/leafy/converter/dummy_converter.rb
124
+ - lib/leafy/converter/integer_converter.rb
125
+ - lib/leafy/converter/string_converter.rb
126
+ - lib/leafy/field.rb
127
+ - lib/leafy/field_value.rb
128
+ - lib/leafy/field_value_collection.rb
129
+ - lib/leafy/mixin/active_record/fields.rb
130
+ - lib/leafy/mixin/active_record/schema.rb
131
+ - lib/leafy/mixin/active_record/shared.rb
132
+ - lib/leafy/mixin/data_accessor.rb
133
+ - lib/leafy/mixin/fields.rb
134
+ - lib/leafy/mixin/poro/fields.rb
135
+ - lib/leafy/mixin/poro/schema.rb
136
+ - lib/leafy/mixin/schema.rb
137
+ - lib/leafy/schema.rb
138
+ - lib/leafy/utils.rb
139
+ - lib/leafy/version.rb
140
+ homepage: https://gibhub.com/estepnv/leafy
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 2.7.7
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: Toolkit for custom attributes in Ruby apps
164
+ test_files: []