Skip to content

Commit

Permalink
Merge pull request #364 from imdrasil/feature/add-as-json
Browse files Browse the repository at this point in the history
Add #to_json to model, view, query and record
  • Loading branch information
imdrasil authored Mar 22, 2021
2 parents e55ca77 + 04b051c commit 94d3971
Show file tree
Hide file tree
Showing 23 changed files with 719 additions and 95 deletions.
27 changes: 14 additions & 13 deletions docs/model_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,30 @@ To make some field nillable tou can use any of the next options:

If you don't want to define all the table fields - pass `false` as second argument (this will disable default strict mapping mode).

`%mapping` defines next methods:
`.mapping` defines next methods:

| method | args | description |
| --- | --- | --- |
| `#initialize` | `Hash(String \| Symbol, DB::Any), NamedTuple, MySql::ResultSet` | constructors |
| `::field_count`| | number of fields |
| `::field_names`| | all fields names |
| `.new` | `Hash(String \| Symbol, DB::Any), NamedTuple, MySql::ResultSet` | constructors |
| `.field_count`| | number of fields |
| `.field_names`| | all fields names |
| `._{{field_name}}` | | helper method for building queries |
| `.coerce_{{field_name}}` | `String` | coerces string to `field_name` type |
| `.primary` | | returns criterion for primary field (query DSL) |
| `.primary_field_name` | | name of primary field |
| `.create` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | creates object, stores it to db and returns it |
| `.create!` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | creates object, stores it to db and returns it; otherwise raise exception |
| `.build` | `Hash(String \| Symbol, DB::Any), NamedTuple` | builds object |
| `.create` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | builds object from hash and saves it to db with all callbacks |
| `.create!` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | builds object from hash and saves it to db with callbacks or raise exception |
| `#{{field_name}}` | | getter |
| `#{{field_name}}_changed?` | | presents whether field is changed |
| `#{{field_name}}!` | | getter with `not_nil!` if `null: true` was passed |
| `#{{field_name}}=`| | setter |
| `::_{{field_name}}` | | helper method for building queries |
| `#{{field_name}}_changed?` | | shows if field was changed |
| `#new_record?` | | returns `true` if record has `nil` primary key (is not stored to db) |
| `#changed?` | | shows if any field was changed |
| `#primary` | | value of primary key field |
| `::primary` | | returns criterion for primary field (query DSL) |
| `::primary_field_name` | | name of primary field |
| `#new_record?` | | returns `true` if record has `nil` primary key (is not stored to db) |
| `::create` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | creates object, stores it to db and returns it |
| `::create!` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | creates object, stores it to db and returns it; otherwise raise exception |
| `::build` | `Hash(String \| Symbol, DB::Any), NamedTuple` | builds object |
| `::create` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | builds object from hash and saves it to db with all callbacks |
| `::create!` | `Hash(String \| Symbol, DB::Any)`, `NamedTuple` | builds object from hash and saves it to db with callbacks or raise exception |
| `#save` | | saves object to db; returns `true` if success and `false` elsewhere |
| `#save!` | | saves object to db; returns `true` if success or rise exception otherwise |
| `#to_h` | | returns hash with all attributes |
Expand Down
12 changes: 12 additions & 0 deletions docs/model_scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ ModelName.all
.order(f1: :asc)
.no_argument_query
```

## Default model scope

To define a default model scope override `.all` method:

```crystal
class Model < Jennifer::Mode::Base
def self.all
super.where { _deleted == false }
end
end
```
42 changes: 35 additions & 7 deletions docs/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ There are multiple approaches to implement model serialization to a required for

## General

Jennifer defines some hidden instance attributes in defined models for own use. Sometimes we would like to operate with a class where we have full access to all defined attributes/methods. For this case the easiest way is to use [JenniferTwin](https://github.com/imdrasil/jennifer_twin). Using it you can dump Jennifer model instance to a one that is totally under your control. One of the cases when this approach may come in handy when we would like to use attribute annotations, like [MessagePack::Serializable](https://github.com/crystal-community/msgpack-crystal) or [JSON::Serializable](https://crystal-lang.org/api/0.31.1/JSON/Serializable.html).
Jennifer defines some hidden instance attributes in defined models for own use. Sometimes we would like to operate with a class where we have full access to all defined attributes/methods. For this case the easiest way is to use [JenniferTwin](https://github.com/imdrasil/jennifer_twin) lib. Using it you can dump Jennifer model instance to a separate object that is totally under your control. One of the cases when this approach may come in handy when ther is a need to use attribute annotations, like [MessagePack::Serializable](https://github.com/crystal-community/msgpack-crystal) or [JSON::Serializable](https://crystal-lang.org/api/0.31.1/JSON/Serializable.html).

```crystal
class User < Jennifer::Model::Base
Expand Down Expand Up @@ -45,17 +45,45 @@ user_twin.to_modal # <User:0x000000000030 id: nil, name: "New Name", age: nil, p

## JSON

For JSON serialization there are 2 options (apart from described above).
For JSON serialization there are 3 options (one of which is described above).

### `Model::Base#to_h`
### `#to_json`

If you need just to dump all non virtual fields to a JSON string - use `Jennifer::Model::Base#to_h` to get `Hash(Symbol, T::AttrType)` (or `#to_str_h` to get `Hash(String, T::AttrType)`) and then `#to_json` to get a JSON string. The disadvantages of this approach are obvious - **all** non virtual fields are serialized. This can be partially resolved by manual deleting/adding entries by keys (as before final serialization we get hash).
Out of the box Jennifer provides `#to_json` method at model and query levels. They allows you to serialize specific records all together with any additional custom properties.

```crystal
user = User.all.first # <User:0x000000000010 id: 1, name: "User 8", age: nil, password_hash: "<hash>">
hash = user.to_h # => {:id => 1, :full_name => "User 8", :age => null}
hash.delete(:age) if hash[:age].nil?
hash.to_json # => %({"id":1,"full_name":"User 8"})
user.to_json # => {"id":1,"name":"User 8","age":null}
```

To specify exact subset of fields that should be serialized use `only` argument

```crystal
user.to_json(only: %w[id name]) # => {"id":1,"name":"User 8"}
# or just
user.to_json(%w[id name])
```

It is possible to specify exception list of fields by `except` argument:

```crystal
user.to_json(except: %w[id]) # => {"name":"User 8","age":null}
```

To extend serialized object you can pass a block:

```crystal
user.to_json(only: %w[id name]) do |json|
json.field "custom", "value"
end # => {"id":1,"name":"User 8","custom":"value}
```

To serialize collection retrieved from the database call `#to_json` method of `Jennifer::Query` directly. It accepts the same arguments as described above.

```crystal
User.all.where { _name.like("%ohn%) }.to_json(except: %w[age]) do |json, record|
json.field "custom", record.age
end # => [{"id":1,"name":"John","custom":23}]
```

### Serializer
Expand Down
6 changes: 3 additions & 3 deletions spec/adapter/base_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -364,15 +364,15 @@ describe Jennifer::Adapter::Base do

describe "#tables_column_count" do
it "returns amount of tables fields" do
match_array(adapter.tables_column_count(["passports", "addresses"]).to_a.map(&.count), [2, 7])
adapter.tables_column_count(["passports", "addresses"]).to_a.map(&.count).should match_array([2, 7])
end

it "returns amount of views fields" do
postgres_only do
match_array(adapter.tables_column_count(["male_contacts", "female_contacts"]).to_a.map(&.count), [9, 10])
adapter.tables_column_count(["male_contacts", "female_contacts"]).to_a.map(&.count).should match_array([9, 10])
end
mysql_only do
match_array(adapter.tables_column_count(["male_contacts"]).to_a.map(&.count), [9])
adapter.tables_column_count(["male_contacts"]).to_a.map(&.count).should match_array([9])
end
end

Expand Down
163 changes: 162 additions & 1 deletion spec/adapter/record_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe Jennifer::Record do
end
end

describe "#/attribute_name/" do
describe "auto generated getter" do
context "without type casting" do
it "generates methods" do
record = get_record
Expand Down Expand Up @@ -67,4 +67,165 @@ describe Jennifer::Record do
end
end
end

describe "#to_json" do
it "includes all fields by default" do
record = get_record
target_hash = db_specific(
mysql: -> do
{
:id => record.id,
:name => record.name,
:age => record.age,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil
}
end,
postgres: -> do
{
:id => record.id,
:name => record.name,
:age => record.age,
:tags => nil,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil
}
end
)
record.to_json.should eq(target_hash.to_json)
end

it "allows to specify *only* argument solely" do
record = get_record
record.to_json(%w[id]).should eq(%({"id":#{record.id}}))
end

it "allows to specify *except* argument solely" do
record = get_record
target_hash = db_specific(
mysql: -> do
{
:name => record.name,
:age => record.age,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil
}
end,
postgres: -> do
{
:name => record.name,
:age => record.age,
:tags => nil,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil
}
end
)
record.to_json(except: %w[id]).should eq(target_hash.to_json)
end

context "with block" do
it "allows to extend json using block" do
executed = false
record = get_record
target_hash = db_specific(
mysql: -> do
{
:id => record.id,
:name => record.name,
:age => record.age,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil,
:custom => "value"
}
end,
postgres: -> do
{
:id => record.id,
:name => record.name,
:age => record.age,
:tags => nil,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil,
:custom => "value"
}
end
)
record.to_json do |json, obj|
executed = true
obj.should eq(record)
json.field "custom", "value"
end.should eq(target_hash.to_json)
executed.should be_true
end

it "respects :only option" do
record = get_record
record.to_json(%w[id]) do |json|
json.field "custom", "value"
end.should eq({id: record.id, custom: "value"}.to_json)
end

it "respects :except option" do
record = get_record
target_hash = db_specific(
mysql: -> do
{
:name => record.name,
:age => record.age,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil,
:custom => "value"
}
end,
postgres: -> do
{
:name => record.name,
:age => record.age,
:tags => nil,
:ballance => nil,
:gender => record.gender,
:created_at => record.created_at,
:updated_at => record.updated_at,
:description => nil,
:user_id => nil,
:custom => "value"
}
end
)

record.to_json(except: %w[id]) do |json|
json.field "custom", "value"
end.should eq(target_hash.to_json)
end
end
end
end
Loading

0 comments on commit 94d3971

Please sign in to comment.