leafy-ruby 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c26d92812d5952076dba88b883f46fea4213c681
4
- data.tar.gz: e6d9d2473e0c31947cdaef28940464aad93f2897
2
+ SHA256:
3
+ metadata.gz: 60d25d9e5f13760ff97626b8baf31cb98fd941cfc250c8585cfd06a758b3c121
4
+ data.tar.gz: 572e02908880e0d8a72f0bd5c15752127b56732196b5ea829d49faf7d3c85744
5
5
  SHA512:
6
- metadata.gz: b16830890b1ae949d5948518e14c89b2a8cf84d8f6e238c772c3bb604f0add0f5bb635766c59b97b496c3e0c43203dd05db9e73aec33f6d9e3e30f84290e0202
7
- data.tar.gz: b595b079f9196968451ea11688f68ca1ba74e8ea5f2d07b468aaf24f8e937736c0ef6300d05ad8afd398587a307feae2fc5c90c9fceab0ec84909f2a2127d1e7
6
+ metadata.gz: aea5d1a9ebf0ebf91e89058ee4749692635fdb7971e8bb6da881d896747776dabb0a66a10515e0addb35a464b62f262a11593d18236d1ce63c2e8bf8334dcc18
7
+ data.tar.gz: fc1538fb57c85f902986c2615656361f0281e1e84ed029f13b7eae11058951790275ce77f39000d5625950cc2fd32b7b41374991f78f72922a26dede91b855ea
@@ -0,0 +1,87 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master, main ]
6
+ pull_request:
7
+ branches: [ master, main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby:
17
+ - '2.4'
18
+ - '2.5'
19
+ - '2.6'
20
+ - '2.7'
21
+ - '3.0'
22
+ - '3.1'
23
+ - '3.2'
24
+ - '3.3'
25
+ include:
26
+ - ruby: 'head'
27
+ continue-on-error: true
28
+ - ruby: 'jruby-9.4'
29
+ continue-on-error: true
30
+ # Ruby 2.2-2.3 are very old and may have stability issues
31
+ - ruby: '2.2'
32
+ continue-on-error: true
33
+ - ruby: '2.3'
34
+ continue-on-error: true
35
+
36
+ continue-on-error: ${{ matrix.continue-on-error || false }}
37
+
38
+ services:
39
+ postgres:
40
+ image: postgres:13
41
+ env:
42
+ POSTGRES_USER: root
43
+ POSTGRES_PASSWORD: 111
44
+ POSTGRES_DB: postgres
45
+ ports:
46
+ - 5432:5432
47
+ options: >-
48
+ --health-cmd pg_isready
49
+ --health-interval 10s
50
+ --health-timeout 5s
51
+ --health-retries 5
52
+
53
+ mysql:
54
+ image: mysql:8.0
55
+ env:
56
+ MYSQL_ROOT_PASSWORD: 111
57
+ MYSQL_DATABASE: leafy_test
58
+ ports:
59
+ - 3306:3306
60
+ options: >-
61
+ --health-cmd "mysqladmin ping"
62
+ --health-interval 10s
63
+ --health-timeout 5s
64
+ --health-retries 5
65
+
66
+ env:
67
+ COVERAGE: 1
68
+
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+
72
+ - name: Set up Ruby
73
+ uses: ruby/setup-ruby@v1
74
+ with:
75
+ ruby-version: ${{ matrix.ruby }}
76
+ bundler-cache: true
77
+
78
+ - name: Run tests
79
+ run: bundle exec rspec
80
+
81
+ - name: Upload coverage to Codecov
82
+ if: matrix.ruby == '3.3'
83
+ uses: codecov/codecov-action@v4
84
+ with:
85
+ files: ./coverage/.resultset.json
86
+ flags: ruby-${{ matrix.ruby }}
87
+ fail_ci_if_error: false
data/.gitignore CHANGED
@@ -14,3 +14,5 @@
14
14
  /.history/*
15
15
  Gemfile.lock
16
16
  .ruby-version
17
+ .docker
18
+ vendor/
@@ -0,0 +1,81 @@
1
+ # Migration from Travis CI to GitHub Actions
2
+
3
+ ## Changes Made
4
+
5
+ ### 1. GitHub Actions Workflow (`.github/workflows/ci.yml`)
6
+ - Created a new CI workflow that tests against multiple Ruby versions
7
+ - **Ruby versions tested**: 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, head, jruby-9.4
8
+ - Set up PostgreSQL and MySQL services (matching your docker-compose setup)
9
+ - Integrated Code Climate test reporter for coverage (only on Ruby 3.3)
10
+ - Allow failures for Ruby head and JRuby (won't block CI)
11
+
12
+ ### 2. Updated `leafy.gemspec`
13
+ - Improved ActiveRecord version selection based on Ruby version:
14
+ - Ruby 2.2-2.4: ActiveRecord ~> 5.2
15
+ - Ruby 2.5-2.6: ActiveRecord ~> 6.0
16
+ - Ruby 2.7+: ActiveRecord ~> 6.1
17
+ - Added pg version constraint `< 2.0` for better compatibility
18
+
19
+ ### 3. Updated `Gemfile`
20
+ - Made Ruby 3.4+ bundled gems conditional (only loaded when RUBY_VERSION >= 3.4.0)
21
+ - This ensures older Ruby versions don't have issues with gems they don't need
22
+
23
+ ### 4. Updated `spec/spec_helper.rb`
24
+ - Made bundled gem requires conditional for Ruby 3.4+
25
+ - Ensures compatibility across all Ruby versions
26
+
27
+ ### 5. Updated `README.md`
28
+ - Replaced Travis CI badge with GitHub Actions badge
29
+
30
+ ## Testing the Setup
31
+
32
+ ### Local Testing
33
+ Ensure tests still pass locally:
34
+ ```bash
35
+ bundle install
36
+ bundle exec rspec
37
+ ```
38
+
39
+ ### Testing with Different Ruby Versions (using Docker)
40
+ ```bash
41
+ # Ruby 2.7
42
+ docker run -it --rm -v $(pwd):/app -w /app ruby:2.7 bash -c "bundle install && bundle exec rspec"
43
+
44
+ # Ruby 3.0
45
+ docker run -it --rm -v $(pwd):/app -w /app ruby:3.0 bash -c "bundle install && bundle exec rspec"
46
+
47
+ # Ruby 3.1
48
+ docker run -it --rm -v $(pwd):/app -w /app ruby:3.1 bash -c "bundle install && bundle exec rspec"
49
+ ```
50
+
51
+ ## What to Do Next
52
+
53
+ 1. **Commit the changes**:
54
+ ```bash
55
+ git add .github/workflows/ci.yml
56
+ git add Gemfile leafy.gemspec spec/spec_helper.rb README.md
57
+ git rm .travis.yml
58
+ git commit -m "Migrate from Travis CI to GitHub Actions"
59
+ ```
60
+
61
+ 2. **Push to GitHub**:
62
+ ```bash
63
+ git push origin master
64
+ ```
65
+
66
+ 3. **Verify the workflow**:
67
+ - Go to your repository on GitHub
68
+ - Click on the "Actions" tab
69
+ - You should see the CI workflow running
70
+ - Check that tests pass for all Ruby versions
71
+
72
+ ## Notes
73
+
74
+ - The workflow runs on every push to `master`/`main` branches and on pull requests
75
+ - Ruby 2.2-2.4 may have limitations with newer gems, so ActiveRecord 5.2 is used
76
+ - Ruby 3.4+ requires explicit bundled gem declarations (erb, logger, mutex_m, etc.)
77
+ - Code Climate coverage is only uploaded from Ruby 3.3 builds to avoid duplicates
78
+
79
+ ## Cleanup
80
+
81
+ You can safely delete `.travis.yml` after confirming GitHub Actions is working.
data/Gemfile CHANGED
@@ -4,3 +4,27 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in leafy.gemspec
6
6
  gemspec
7
+
8
+ # Ruby 2.5+ bundled gems needed by ActiveRecord 6.0+
9
+ if RUBY_VERSION >= "2.5.0"
10
+ gem 'logger'
11
+ end
12
+
13
+ # Ruby 3.0+ additional bundled gems
14
+ if RUBY_VERSION >= "3.0.0"
15
+ gem 'mutex_m'
16
+ gem 'base64'
17
+ gem 'benchmark' # Needed by ActiveSupport in Ruby 3.3+/4.x
18
+ end
19
+
20
+ # Ruby 3.1+ additional bundled gems
21
+ if RUBY_VERSION >= "3.1.0"
22
+ gem 'csv'
23
+ end
24
+
25
+ # Ruby 3.4+ additional bundled gems
26
+ if RUBY_VERSION >= "3.4.0"
27
+ gem 'erb'
28
+ gem 'bigdecimal'
29
+ gem 'drb'
30
+ end
data/README.md CHANGED
@@ -1,40 +1,78 @@
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)
1
+ # Leafy [![CI](https://github.com/estepnv/leafy/actions/workflows/ci.yml/badge.svg)](https://github.com/estepnv/leafy/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/estepnv/leafy/branch/master/graph/badge.svg)](https://codecov.io/gh/estepnv/leafy) [![Maintainability](https://api.codeclimate.com/v1/badges/5108d8a1ac5e2915f30f/maintainability)](https://codeclimate.com/github/estepnv/leafy/maintainability)
2
2
 
3
3
  A toolkit for dynamic custom attributes for Ruby applications.
4
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
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Supported Data Types](#supported-data-types)
9
+ - [Installation](#installation)
10
+ - [Requirements](#requirements)
11
+ - [Quick Start](#quick-start)
12
+ - [Plain Ruby Objects (PORO)](#plain-ruby-objects-poro)
13
+ - [ActiveRecord Integration](#activerecord-integration)
14
+ - [Configuration](#configuration)
15
+ - [Custom Field Types](#custom-field-types)
16
+ - [Best Practices](#best-practices)
17
+ - [API Reference](#api-reference)
18
+ - [Troubleshooting](#troubleshooting)
19
+ - [Contributing](#contributing)
20
+ - [License](#license)
21
+
22
+ ## Features
23
+
24
+ * **Simple modular design** - Load only what you need
25
+ * **JSON-backed storage** - Store custom fields as JSON with your models, avoiding expensive JOIN queries
26
+ * **PostgreSQL support** - Native support for `json` and `jsonb` column types
27
+ * **Type safety** - Automatic type inference and validation for custom field data
28
+ * **Extensible** - Add your own custom field types with converters
29
+ * **Thread-safe** - Safe for concurrent access
30
+
31
+ ## Supported Data Types
32
+
33
+ - `string` - String values
34
+ - `integer` - Integer numbers
35
+ - `double` - Floating point numbers
36
+ - `datetime` - `Time` instances (stored as ISO8601)
37
+ - `date` - `Date` instances (stored as ISO8601)
38
+ - `bool` - Boolean values (`true`/`false`)
39
+ - `dummy` - Pass-through type (no conversion)
40
+
41
+ ## Installation
42
+
43
+ Add Leafy to your Gemfile:
21
44
 
22
45
  ```ruby
23
46
  gem 'leafy-ruby'
24
47
  ```
25
48
 
26
- **Plain Ruby app**
49
+ Then run:
50
+
51
+ ```bash
52
+ bundle install
53
+ ```
54
+
55
+ ## Requirements
56
+
57
+ - Ruby 2.7 or higher
58
+ - ActiveRecord 6.0+ (optional, only if using ActiveRecord integration)
59
+
60
+ ## Quick Start
61
+
62
+ ## Quick Start
63
+
64
+ ### Plain Ruby Objects (PORO)
27
65
 
28
- Include "Plain old ruby object" mixin into your class definition to start using leafy
66
+ For plain Ruby objects, include the `:poro` mixin and provide a `leafy_data` accessor:
29
67
 
30
68
  ```ruby
31
- class SchemaHost < ActiveRecord::Base
69
+ class SchemaHost
32
70
  include Leafy::Mixin::Schema[:poro]
33
71
 
34
72
  attr_accessor :leafy_data
35
73
  end
36
74
 
37
- class FieldsHost < ActiveRecord::Base
75
+ class FieldsHost
38
76
  include Leafy::Mixin::Fields[:poro]
39
77
 
40
78
  attr_accessor :leafy_data
@@ -42,37 +80,67 @@ class FieldsHost < ActiveRecord::Base
42
80
  end
43
81
  ```
44
82
 
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
83
+ **Schema mixin provides:**
50
84
 
51
- Fields mixin:
85
+ - `#leafy_fields` - Returns a `Leafy::Schema` instance for iterating through custom field definitions
86
+ - `#leafy_fields=` - Schema setter method
87
+ - `#leafy_fields_attributes=` - Nested attributes setter method
52
88
 
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
89
+ **Fields mixin provides:**
56
90
 
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.
91
+ - `#leafy_values` - Returns a hash of field values
92
+ - `#leafy_values=` - Assigns custom field values
93
+ - `#leafy_field_values` - Returns a `Leafy::FieldValueCollection` for fine-grained control
60
94
 
95
+ **Important:** Leafy is stateless. Changing a Schema instance won't automatically update your model. You must explicitly assign the schema or attributes to persist changes.
61
96
 
97
+ #### Example Usage
62
98
 
63
99
  ```ruby
100
+ # Create a schema host
64
101
  host = SchemaHost.new
102
+
103
+ # Define custom fields using attributes
65
104
  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 } }
105
+ {
106
+ name: "Field 1",
107
+ type: :integer,
108
+ id: "id_1",
109
+ metadata: { default: 1, placeholder: "Enter an integer", required: true }
110
+ },
111
+ {
112
+ name: "Field 2",
113
+ type: :string,
114
+ id: "id_2",
115
+ metadata: { default: "", placeholder: "Enter value" }
116
+ },
117
+ {
118
+ name: "Field 3",
119
+ type: :datetime,
120
+ id: "id_3",
121
+ metadata: { order: 10000 }
122
+ }
69
123
  ]
70
124
 
71
- # or build schema yourself
72
-
73
- field_1 = Leafy::Field.new(name: "Field 1", type: :integer, id: "id_1", metadata: { default: 1, placeholder: "enter an integer", required: true })
74
- field_2 = Leafy::Field.new(name: "Field 2", type: :string, id: "id_2", metadata: { default: "", placeholder: "enter value" })
75
- field_3 = Leafy::Field.new(name: "Field 3", type: :datetime, id: "id_3", metadata: { order: 10000 })
125
+ # Or build the schema manually
126
+ field_1 = Leafy::Field.new(
127
+ name: "Field 1",
128
+ type: :integer,
129
+ id: "id_1",
130
+ metadata: { default: 1, placeholder: "Enter an integer", required: true }
131
+ )
132
+ field_2 = Leafy::Field.new(
133
+ name: "Field 2",
134
+ type: :string,
135
+ id: "id_2",
136
+ metadata: { default: "", placeholder: "Enter value" }
137
+ )
138
+ field_3 = Leafy::Field.new(
139
+ name: "Field 3",
140
+ type: :datetime,
141
+ id: "id_3",
142
+ metadata: { order: 10000 }
143
+ )
76
144
 
77
145
  schema = Leafy::Schema.new
78
146
  schema << field_1
@@ -81,31 +149,47 @@ schema << field_3
81
149
 
82
150
  host.leafy_fields = schema
83
151
 
84
- # after that reference schema for fields target instance
85
-
152
+ # Use the schema with a fields host
86
153
  target = FieldsHost.new
87
154
  target.leafy_fields = host.leafy_fields
88
- target.leafy_values
89
155
 
156
+ # Initial values are nil
157
+ target.leafy_values
90
158
  # => { "id_1" => nil, "id_2" => nil, "id_3" => nil }
91
159
 
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
160
+ # Set values (unknown fields are ignored)
161
+ target.leafy_values = {
162
+ "id_1" => 123,
163
+ "id_2" => "test",
164
+ "id_3" => Time.new(2018, 10, 10, 10, 10, 10, "+03:00"),
165
+ "junk" => "ignored"
166
+ }
94
167
 
95
- # => { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00") }
168
+ target.leafy_values
169
+ # => { "id_1" => 123, "id_2" => "test", "id_3" => 2018-10-10 07:10:10 UTC }
96
170
  ```
97
171
 
98
- **ActiveRecord**
172
+ ### ActiveRecord Integration
173
+
174
+ #### 1. Create a migration
99
175
 
100
- Add migration
101
176
  ```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: {}
177
+ class AddLeafyData < ActiveRecord::Migration[6.1]
178
+ def change
179
+ # For text/string storage (all databases)
180
+ add_column :schema_hosts, :leafy_data, :text, null: false, default: "{}"
181
+ add_column :fields_hosts, :leafy_data, :text, null: false, default: "{}"
182
+
183
+ # For PostgreSQL with native JSON support (recommended)
184
+ # add_column :schema_hosts, :leafy_data, :jsonb, null: false, default: {}
185
+ # add_column :fields_hosts, :leafy_data, :jsonb, null: false, default: {}
186
+ # add_index :schema_hosts, :leafy_data, using: :gin
187
+ # add_index :fields_hosts, :leafy_data, using: :gin
188
+ end
189
+ end
106
190
  ```
107
191
 
108
- Update your models
192
+ #### 2. Update your models
109
193
 
110
194
  ```ruby
111
195
  class SchemaHost < ActiveRecord::Base
@@ -120,74 +204,218 @@ class FieldsHost < ActiveRecord::Base
120
204
  end
121
205
  ```
122
206
 
207
+ #### 3. Usage
208
+
123
209
  ```ruby
210
+ # Create a schema host with custom fields
124
211
  host = SchemaHost.create(
125
212
  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 } }
213
+ {
214
+ name: "Field 1",
215
+ type: :integer,
216
+ id: "id_1",
217
+ metadata: { default: 1, placeholder: "Enter an integer", required: true }
218
+ },
219
+ {
220
+ name: "Field 2",
221
+ type: :string,
222
+ id: "id_2",
223
+ metadata: { default: "", placeholder: "Enter value" }
224
+ },
225
+ {
226
+ name: "Field 3",
227
+ type: :datetime,
228
+ id: "id_3",
229
+ metadata: { order: 10000 }
230
+ }
129
231
  ]
130
232
  )
131
233
 
234
+ # Create a fields host and set values
132
235
  target = FieldsHost.create(schema_host: host)
133
- target.leafy_values
134
236
 
237
+ target.leafy_values
135
238
  # => { "id_1" => nil, "id_2" => nil, "id_3" => nil }
136
239
 
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" }
240
+ target.leafy_values = {
241
+ "id_1" => 123,
242
+ "id_2" => "test",
243
+ "id_3" => Time.new(2018, 10, 10, 10, 10, 10, "+03:00"),
244
+ "junk" => "ignored"
245
+ }
138
246
  target.save!
139
247
  target.reload
140
248
 
141
249
  target.leafy_values
142
-
143
- # => { "id_1": 123, "id_2": "test", "id_3": Time.new(2018,10,10, 10,10,10, "+03:00") }
250
+ # => { "id_1" => 123, "id_2" => "test", "id_3" => 2018-10-10 07:10:10 UTC }
144
251
  ```
145
252
 
146
253
  ## Configuration
147
254
 
148
- In you initialization code
255
+ ### Rails Setup
256
+
257
+ If you get a `NameError: uninitialized constant` error in Rails, create an initializer:
149
258
 
150
259
  ```ruby
151
- class MyLovelyCoder
260
+ # config/initializers/leafy.rb
261
+ require 'leafy'
262
+ ```
263
+
264
+ ### Custom Coder
265
+
266
+ By default, Leafy uses the JSON module for serialization. You can configure a custom coder (e.g., Oj for better performance):
267
+
268
+ ```ruby
269
+ # config/initializers/leafy.rb
270
+ require 'leafy'
271
+ require 'oj'
272
+
273
+ class OjCoder
152
274
  def dump(data)
153
- "lovely_#{data}"
275
+ Oj.dump(data)
154
276
  end
155
277
 
156
278
  def load(data)
157
- data.split("_")[1]
279
+ Oj.load(data)
158
280
  end
159
281
  end
160
282
 
161
283
  Leafy.configure do |config|
162
- # you may wonna use oj instead
163
- config.coder = MyLovelyCoder.new
284
+ config.coder = OjCoder.new
164
285
  end
165
286
  ```
166
287
 
167
- ## Adding your own types
288
+ **Note:** Your coder must implement both `#dump` and `#load` instance methods.
168
289
 
169
- Leafy allows adding your own data types
170
- 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.
290
+ ## Custom Field Types
171
291
 
172
- Converter instance has to implement `#dump` and `#load` methods
292
+ Leafy allows you to add your own custom data types by registering converters.
293
+
294
+ ### Creating a Converter
295
+
296
+ A converter is responsible for serializing (dump) and deserializing (load) your custom type. It must implement both `#dump` and `#load` instance methods:
173
297
 
174
298
  ```ruby
175
- class MyComplexTypeConverter
176
- def self.load(json_string)
177
- # parsing logic
178
- return MyComplexType.new(parsed_data)
299
+ class MoneyConverter
300
+ def dump(value)
301
+ return nil if value.nil?
302
+ # Convert Money object to cents for storage
303
+ value.cents.to_s
179
304
  end
180
305
 
181
- def self.dump(my_complex_type_instance)
182
- # serializing logic
183
- return json
306
+ def load(value)
307
+ return nil if value.nil?
308
+ # Convert cents back to Money object
309
+ Money.new(value.to_i)
184
310
  end
185
311
  end
186
312
 
187
- Leafy.register_converter(:complex_type, MyComplexTypeConverter)
313
+ # Register the converter
314
+ Leafy.register_converter(:money, MoneyConverter.new)
315
+ ```
316
+
317
+ ### Using Custom Types
318
+
319
+ ```ruby
320
+ schema = Leafy::Schema.new
321
+ schema << Leafy::Field.new(
322
+ name: "Price",
323
+ type: :money, # Your custom type
324
+ id: "price_field"
325
+ )
326
+
327
+ host.leafy_fields = schema
328
+ target.leafy_fields = schema
329
+
330
+ target.leafy_values = { "price_field" => Money.new(1999) }
331
+ target.leafy_values["price_field"]
332
+ # => #<Money cents=1999>
333
+ ```
334
+
335
+ ## Best Practices
336
+
337
+ ### Field IDs
338
+
339
+ - Use stable, unique IDs for fields (UUIDs are generated automatically if not provided)
340
+ - Don't change field IDs after data has been stored
341
+ - Field IDs are the key for storing values - changing them will lose existing data
342
+
343
+ ### Metadata
344
+
345
+ The `metadata` hash is completely flexible - store any additional information you need:
346
+
347
+ ```ruby
348
+ metadata: {
349
+ default: "some default",
350
+ placeholder: "Help text",
351
+ required: true,
352
+ order: 100,
353
+ validation_rules: { min: 0, max: 100 },
354
+ custom_property: "anything you want"
355
+ }
188
356
  ```
189
357
 
358
+ ### Performance Tips
359
+
360
+ - Use PostgreSQL `jsonb` columns for better query performance and indexing
361
+ - Keep the number of custom fields reasonable (< 100 per model)
362
+ - Use GIN indexes on jsonb columns for field queries
363
+ - Consider using Oj or other fast JSON libraries as your coder
364
+
365
+ ### Thread Safety
366
+
367
+ Leafy's class-level configuration and converter registry are thread-safe. You can safely register converters and configure Leafy from multiple threads or in multi-threaded web servers (Puma, Sidekiq, etc.).
368
+
369
+ ## API Reference
370
+
371
+ ### Schema Methods
372
+
373
+ - `Leafy::Schema.new(fields_array)` - Create a new schema
374
+ - `#push(field)` / `#<<(field)` - Add a field to the schema
375
+ - `#[](identifier)` - Find a field by ID
376
+ - `#ids` - Get array of all field IDs
377
+ - `#each` - Iterate through fields (Enumerable)
378
+ - `#serializable_hash` - Convert to hash representation
379
+ - `Leafy::Schema.dump(schema)` - Serialize to JSON string
380
+ - `Leafy::Schema.load(json_string)` - Deserialize from JSON string
381
+
382
+ ### Field Methods
383
+
384
+ - `Leafy::Field.new(name:, type:, id:, metadata:)` - Create a new field
385
+ - `#name` - Field display name
386
+ - `#type` - Field type symbol
387
+ - `#id` - Unique field identifier
388
+ - `#metadata` - Custom metadata hash
389
+ - `#serializable_hash` - Convert to hash representation
390
+
391
+ ### FieldValueCollection Methods
392
+
393
+ - `#values` - Get hash of all field values
394
+ - `#values=` - Set field values from hash
395
+ - `#each` - Iterate through field values (Enumerable)
396
+ - `#[](index)` - Access by array index
397
+ - `#size` / `#count` - Number of fields
398
+
399
+ ## Troubleshooting
400
+
401
+ ### NameError: uninitialized constant Leafy
402
+
403
+ **Solution:** Add `require 'leafy'` to your initializer file.
404
+
405
+ ### Values not persisting in ActiveRecord
406
+
407
+ **Solution:** Make sure you call `save` or `save!` after setting `leafy_values`. Leafy setters update the model but don't automatically save.
408
+
409
+ ### Custom converter not working
410
+
411
+ **Solution:** Ensure your converter:
412
+ 1. Implements both `#dump` and `#load` as **instance methods** (not class methods)
413
+ 2. Is registered before use: `Leafy.register_converter(:my_type, MyConverter.new)`
414
+ 3. Handles `nil` values appropriately
415
+
416
+ ### Type mismatch errors
190
417
 
418
+ **Solution:** Converters will attempt to coerce values. For strict validation, implement it in your converter's `#dump` or `#load` methods.
191
419
 
192
420
  ## Contributing
193
421
 
@@ -0,0 +1,23 @@
1
+ version: "2.4"
2
+
3
+ services:
4
+ mysql:
5
+ image: mysql:8.0.20
6
+ environment:
7
+ - MYSQL_ROOT_PASSWORD=111
8
+ - MYSQL_PORT=3306
9
+ - MYSQL_LOWER_CASE_TABLE_NAMES=0
10
+ ports:
11
+ - 3306:3306
12
+ volumes:
13
+ - ./.docker/mysql/master/data:/var/lib/mysql:delegated
14
+ postgresql:
15
+ image: postgres:latest
16
+ environment:
17
+ - POSTGRES_PASSWORD=111
18
+ - POSTGRES_USER=root
19
+ - PGDATA=/var/lib/postgresql/data
20
+ ports:
21
+ - 5432:5432
22
+ volumes:
23
+ - ./.docker/postgres/data:/var/lib/postgresql/data/:delegated
data/leafy.gemspec CHANGED
@@ -32,17 +32,40 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "rspec", "~> 3.0"
33
33
  spec.add_development_dependency "simplecov", '~> 0.17.1'
34
34
 
35
- if RUBY_VERSION >= "2.5.0"
36
- spec.add_development_dependency "activerecord", "~> 6.0"
35
+ # ActiveRecord version based on Ruby version
36
+ if RUBY_VERSION >= "2.7.0"
37
+ spec.add_development_dependency "activerecord", "~> 6.1.0"
38
+ elsif RUBY_VERSION >= "2.5.0"
39
+ spec.add_development_dependency "activerecord", "~> 6.0.0"
37
40
  else
38
- spec.add_development_dependency "activerecord", "~> 5.2"
41
+ spec.add_development_dependency "activerecord", "~> 5.2.0"
39
42
  end
40
43
 
41
44
  if RUBY_ENGINE == "jruby"
42
- spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "51"
43
- spec.add_development_dependency "pg_jruby"
45
+ # JDBC adapter version must match ActiveRecord version
46
+ if RUBY_VERSION >= "2.7.0"
47
+ spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "~> 61.0"
48
+ elsif RUBY_VERSION >= "2.5.0"
49
+ spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "~> 60.0"
50
+ else
51
+ spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "~> 52.0"
52
+ end
53
+ spec.add_development_dependency "jdbc-postgres"
44
54
  else
45
- spec.add_development_dependency "sqlite3"
46
- spec.add_development_dependency "pg"
55
+ # sqlite3 version based on Ruby version (1.4+ requires Ruby 2.5+)
56
+ if RUBY_VERSION >= "2.5.0"
57
+ spec.add_development_dependency "sqlite3", "~> 1.4"
58
+ else
59
+ spec.add_development_dependency "sqlite3", "~> 1.3.0"
60
+ end
61
+ # pg version constraint for older Ruby versions
62
+ if RUBY_VERSION >= "2.5.0"
63
+ spec.add_development_dependency "pg", "< 2.0"
64
+ elsif RUBY_VERSION >= "2.4.0"
65
+ spec.add_development_dependency "pg", "~> 1.0"
66
+ else
67
+ # Ruby 2.2-2.3 need older pg version to avoid segfaults
68
+ spec.add_development_dependency "pg", "~> 0.21.0"
69
+ end
47
70
  end
48
71
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Leafy
2
4
  module Coder
3
5
  Default = JSON
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Leafy
2
4
  module Coder
3
5
  class Mock
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module Leafy
@@ -11,9 +13,9 @@ module Leafy
11
13
  def coder=(value)
12
14
  if value.respond_to?(:dump) && value.respond_to?(:load)
13
15
  @coder = value
16
+ else
17
+ raise ArgumentError, "coder must implement #dump and #load"
14
18
  end
15
-
16
- raise ArgumentError, "coder must implement #dump and #load"
17
19
  end
18
20
  end
19
21
  end
@@ -13,7 +13,15 @@ module Leafy
13
13
  def load(value)
14
14
  return if value.nil?
15
15
 
16
- target = value.respond_to?(:downcase) ? (value.downcase rescue nil) : value
16
+ target = if value.respond_to?(:downcase)
17
+ begin
18
+ value.downcase
19
+ rescue StandardError
20
+ nil
21
+ end
22
+ else
23
+ value
24
+ end
17
25
  return true if ["1", "true", "t", 1, "yes", "y", true].include?(target)
18
26
  return false if ["0", "false", "f", 0, "no", "n", false].include?(target)
19
27
 
@@ -9,9 +9,9 @@ module Leafy
9
9
  def dump(value)
10
10
  return if value.nil?
11
11
 
12
- target = value.dup
12
+ target = value
13
13
  target = load(target) if target.is_a?(String)
14
- target = target.dup.to_date if target.is_a?(Time)
14
+ target = target.to_date if target.is_a?(Time)
15
15
 
16
16
  unless target.is_a?(Date)
17
17
  raise(ArgumentError, "is not a Date object")
@@ -9,7 +9,7 @@ module Leafy
9
9
  def dump(value)
10
10
  return if value.nil?
11
11
 
12
- target = value.dup
12
+ target = value
13
13
  target = load(target) if target.is_a?(String)
14
14
 
15
15
  raise(ArgumentError, "is not a Time object") unless target.is_a?(Time)
@@ -5,7 +5,7 @@ module Leafy
5
5
  attr_accessor :id, :name, :type, :raw, :converter
6
6
 
7
7
  def initialize(attributes)
8
- attributes = attributes.dup.to_a.map { |pair| [pair[0].to_sym, pair[1]]}.to_h
8
+ attributes = attributes.transform_keys(&:to_sym)
9
9
 
10
10
  self.id = attributes.fetch(:id)
11
11
  self.name = attributes.fetch(:name)
@@ -24,12 +24,12 @@ module Leafy
24
24
  field_value_list = leafy_field_values
25
25
  field_value_list.values = attributes
26
26
 
27
- self.leafy_data = field_value_list.dump
27
+ self._leafy_data = field_value_list.dump
28
28
  end
29
29
 
30
30
  def leafy_field_values
31
31
  field_value_collection = ::Leafy::FieldValueCollection.new(leafy_fields, ar_json: activerecord_json_column?)
32
- field_value_collection.load(leafy_data || '{}')
32
+ field_value_collection.load(_leafy_data || '{}')
33
33
  field_value_collection
34
34
  end
35
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Leafy
2
4
  module Mixin
3
5
  module DataAccessor
data/lib/leafy/schema.rb CHANGED
@@ -39,7 +39,9 @@ module Leafy
39
39
  end
40
40
 
41
41
  def self.load(data)
42
- Schema.new(JSON.parse(data))
42
+ parsed = JSON.parse(data)
43
+ raise(ArgumentError, "Schema data must be an array") unless parsed.is_a?(Array)
44
+ Schema.new(parsed)
43
45
  end
44
46
 
45
47
  alias :<< :push
data/lib/leafy/utils.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Leafy
2
4
  module Utils
3
5
  def self.symbolize_keys(hash)
data/lib/leafy/version.rb CHANGED
@@ -7,8 +7,8 @@ module Leafy
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 0
10
- MINOR = 1
11
- TINY = 1
10
+ MINOR = 2
11
+ TINY = 0
12
12
  PRE = nil
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
data/lib/leafy.rb CHANGED
@@ -6,7 +6,7 @@ require "leafy/field"
6
6
  require "leafy/schema"
7
7
  require "leafy/field_value"
8
8
  require "leafy/field_value_collection"
9
- Dir[File.expand_path("../leafy/converter/**/*.rb", __FILE__)].each { |f| require f }
9
+ Dir[File.join(__dir__, "leafy/converter/**/*.rb")].each { |f| require f }
10
10
  require "leafy/mixin/schema"
11
11
  require "leafy/mixin/fields"
12
12
  require "leafy/coder/default"
@@ -16,13 +16,19 @@ require "leafy/configuration"
16
16
 
17
17
  # module definition
18
18
  module Leafy
19
+ @config_mutex = Mutex.new
20
+ @converters_mutex = Mutex.new
19
21
 
20
22
  def self.configure
21
23
  yield configuration if block_given?
22
24
  end
23
25
 
24
26
  def self.configuration
25
- @config ||= Leafy::Configuration.new
27
+ return @config if defined?(@config) && @config
28
+
29
+ @config_mutex.synchronize do
30
+ @config ||= Leafy::Configuration.new
31
+ end
26
32
  end
27
33
 
28
34
  def self.register_converter(name, converter)
@@ -36,7 +42,11 @@ module Leafy
36
42
  end
37
43
 
38
44
  def self.converters
39
- @converters ||= {}
45
+ return @converters if defined?(@converters) && @converters
46
+
47
+ @converters_mutex.synchronize do
48
+ @converters ||= {}
49
+ end
40
50
  end
41
51
  end
42
52
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leafy-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeny Stepanov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2020-02-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rspec
@@ -44,42 +43,42 @@ dependencies:
44
43
  requirements:
45
44
  - - "~>"
46
45
  - !ruby/object:Gem::Version
47
- version: '5.2'
46
+ version: 6.1.0
48
47
  type: :development
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
- version: '5.2'
53
+ version: 6.1.0
55
54
  - !ruby/object:Gem::Dependency
56
55
  name: sqlite3
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
- - - ">="
58
+ - - "~>"
60
59
  - !ruby/object:Gem::Version
61
- version: '0'
60
+ version: '1.4'
62
61
  type: :development
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
- - - ">="
65
+ - - "~>"
67
66
  - !ruby/object:Gem::Version
68
- version: '0'
67
+ version: '1.4'
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: pg
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
- - - ">="
72
+ - - "<"
74
73
  - !ruby/object:Gem::Version
75
- version: '0'
74
+ version: '2.0'
76
75
  type: :development
77
76
  prerelease: false
78
77
  version_requirements: !ruby/object:Gem::Requirement
79
78
  requirements:
80
- - - ">="
79
+ - - "<"
81
80
  - !ruby/object:Gem::Version
82
- version: '0'
81
+ version: '2.0'
83
82
  description: |2
84
83
  Leafy is toolkit that allows you to integrate dynamic custom attributes functionality into your ruby application.
85
84
 
@@ -93,15 +92,18 @@ executables:
93
92
  extensions: []
94
93
  extra_rdoc_files: []
95
94
  files:
95
+ - ".github/workflows/ci.yml"
96
96
  - ".gitignore"
97
97
  - ".rspec"
98
98
  - ".travis.yml"
99
+ - GITHUB_ACTIONS_MIGRATION.md
99
100
  - Gemfile
100
101
  - LICENSE.txt
101
102
  - README.md
102
103
  - Rakefile
103
104
  - bin/console
104
105
  - bin/setup
106
+ - docker-compose.yml
105
107
  - leafy.gemspec
106
108
  - lib/leafy.rb
107
109
  - lib/leafy/coder/default.rb
@@ -132,7 +134,6 @@ homepage: https://github.com/estepnv/leafy
132
134
  licenses:
133
135
  - MIT
134
136
  metadata: {}
135
- post_install_message:
136
137
  rdoc_options: []
137
138
  require_paths:
138
139
  - lib
@@ -147,9 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
148
  - !ruby/object:Gem::Version
148
149
  version: '0'
149
150
  requirements: []
150
- rubyforge_project:
151
- rubygems_version: 2.5.2.3
152
- signing_key:
151
+ rubygems_version: 3.6.9
153
152
  specification_version: 4
154
153
  summary: Toolkit for custom attributes in Ruby apps
155
154
  test_files: []