Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: JSON and YAML migration (Mapping -> Serializable) #28

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1be5f82
style: add further annotations for model.cr
dukenguyenxyz Oct 19, 2020
72a381d
refactor: migrate to json::serializable and allow post initialize met…
dukenguyenxyz Oct 20, 2020
77587b5
refactor: from_trusted_json migrate to json::serialize
dukenguyenxyz Oct 20, 2020
11de8c5
feat: allow converter from_json
dukenguyenxyz Oct 20, 2020
e069611
style: uncomment converter specs
dukenguyenxyz Oct 20, 2020
be91c45
feat: json_ignore @{{name}}_changed, all specs pass
dukenguyenxyz Oct 20, 2020
0c0ec61
style: uncomment specs and edit spec files
dukenguyenxyz Oct 20, 2020
21f0b31
refactor: remove unnecessary code
dukenguyenxyz Oct 20, 2020
5d55afc
refactor: remove from_json_new
dukenguyenxyz Jan 18, 2021
6dd1ec4
refactor: use &.tap pattern for from_json and from_yaml
dukenguyenxyz Jan 19, 2021
112f4e1
feat: add YAML::Field annotations next to JSON::Field
dukenguyenxyz Jan 19, 2021
1d846c7
chore: use original to_json spec
dukenguyenxyz Jan 19, 2021
46fa1c1
chore: replace to_json spec
dukenguyenxyz Jan 19, 2021
72ec666
chore: update shard version and prune shards
dukenguyenxyz Jan 19, 2021
7444452
fix: ignore {field}_changed and {field}_was
caspiano Jan 20, 2021
d743298
feat: use None for non-nillable fields
caspiano Jan 20, 2021
f265b34
feat: remove {{name}}_default for unnilable type
dukenguyenxyz Jan 20, 2021
665f877
feat: allow from_json and from_trusted_json with root params
dukenguyenxyz Jan 20, 2021
baa6428
feat: allow json and yaml annotations, add specs for from_json with r…
dukenguyenxyz Jan 20, 2021
000875c
feat: json and yaml annotations tags working with specs
dukenguyenxyz Jan 20, 2021
48bdb76
chore: fix minor unpassing spec in ci
dukenguyenxyz Jan 20, 2021
321c948
feat: allow assign_attributes with json root
dukenguyenxyz Jan 20, 2021
41b6d35
refactor: use getter? {{name.var}}_present instead of checking manually
dukenguyenxyz Jan 20, 2021
26b817b
fix(validation): add ignore annotation for validation errors
caspiano Jan 21, 2021
2b13828
docs(src/active-model/model.cr)
dukenguyenxyz Feb 21, 2021
90055b0
style(src/active-model/model.cr)
dukenguyenxyz Feb 21, 2021
fabd812
style(src/active-model/model.cr)
dukenguyenxyz Feb 21, 2021
dd8cd4a
docs(model.cr): improve Model.from_json docs
dukenguyenxyz Feb 21, 2021
e5e5300
fix(model.cr): annotate ignore on @{{name}}_was on first init after 0…
dukenguyenxyz Feb 21, 2021
44600ae
feat(model.cr): add type signature for def {{name}}_change
dukenguyenxyz Feb 21, 2021
9514338
docs(model.cr): template def {{name.var.id}}_default var value
dukenguyenxyz Feb 21, 2021
de197fd
chore(shard.yml): user caspiano/http-params-serializable
dukenguyenxyz Feb 21, 2021
477cfbc
chore(shard.yml): add additional contributor
dukenguyenxyz Feb 21, 2021
49707c5
Merge branch 'master' into json_yaml
dukenguyenxyz Feb 21, 2021
a8e707f
style: re-order minor methods and macros to masters position
dukenguyenxyz Feb 21, 2021
8d23dee
Merge branch 'json_yaml' of https://github.com/spider-gazelle/active-…
dukenguyenxyz Feb 21, 2021
8dfa41d
Merge branch 'master' into json_yaml
dukenguyenxyz Apr 12, 2021
716892f
test(model_spe.cr): enum::ValueConverter out of scope generics
dukenguyenxyz Apr 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
name: active-model
version: 2.0.2
version: 2.1.0
dukenguyenxyz marked this conversation as resolved.
Show resolved Hide resolved

dependencies:
http-params-serializable:
github: vladfaust/http-params-serializable
version: ~> 0.3
json_mapping:
github: crystal-lang/json_mapping.cr
yaml_mapping:
github: crystal-lang/yaml_mapping.cr

development_dependencies:
ameba:
Expand Down
4 changes: 2 additions & 2 deletions spec/model_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,10 @@ describe ActiveModel::Model do
describe "serialization" do
it "should support to_json" do
i = Inheritance.new
i.to_json.should eq "{\"boolean\":true,\"string\":\"hello\",\"integer\":45}"
JSON.parse(i.to_json).should eq JSON.parse("{\"boolean\":true,\"string\":\"hello\",\"integer\":45}")

i.no_default = "test"
i.to_json.should eq "{\"boolean\":true,\"string\":\"hello\",\"integer\":45,\"no_default\":\"test\"}"
JSON.parse(i.to_json).should eq JSON.parse("{\"boolean\":true,\"string\":\"hello\",\"integer\":45,\"no_default\":\"test\"}")
end
end

Expand Down
236 changes: 117 additions & 119 deletions src/active-model/model.cr
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
require "http/params"
require "json"
require "json_mapping"
require "yaml"
require "yaml_mapping"
require "http-params-serializable/ext"

require "http/params"
require "http-params-serializable/ext"
require "./http-params"

abstract class ActiveModel::Model
include JSON::Serializable
include YAML::Serializable

# :nodoc:
FIELD_MAPPINGS = {} of Nil => Nil

module Missing
extend self
end

# Stub methods to prevent compiler errors
def apply_defaults; end

def changed?; end

def clear_changes_information; end

def changed_attributes; end

protected def validation_error; end

macro inherited
# Macro level constants

Expand All @@ -38,25 +50,14 @@ abstract class ActiveModel::Model
__customize_orm__
{% unless @type.abstract? %}
__track_changes__
__map_json__
__create_initializer__
__getters__
__nilability_validation__
__map_json__
{% end %}
end
end

# Stub methods to prevent compiler errors
def apply_defaults; end

def changed?; end

def clear_changes_information; end

def changed_attributes; end

protected def validation_error; end

# :nodoc:
macro __process_attributes__
{% klasses = @type.ancestors %}
Expand Down Expand Up @@ -105,6 +106,8 @@ abstract class ActiveModel::Model
{% end %}
end

# # Methods that return attributes
dukenguyenxyz marked this conversation as resolved.
Show resolved Hide resolved

# Returns a Hash of all the attribute values
def attributes
{
Expand Down Expand Up @@ -141,6 +144,7 @@ abstract class ActiveModel::Model
} {% if PERSIST.empty? %} of Nil => Nil {% end %}
end

# Deserialize from JSON if value is available in the payload
dukenguyenxyz marked this conversation as resolved.
Show resolved Hide resolved
def assign_attributes(
{% for name, opts in FIELDS %}
{{name.id}} : {{opts[:klass]}} | Missing = Missing,
Expand Down Expand Up @@ -280,12 +284,15 @@ abstract class ActiveModel::Model
apply_defaults
end

# Override the map json
# Setters
{% for name, opts in FIELDS %}
# {{name}} setter
def {{name}}=(value : {{opts[:klass]}})
if !@{{name}}_changed && @{{name}} != value
@[JSON::Field(ignore: true)]
@[YAML::Field(ignore: true)]
dukenguyenxyz marked this conversation as resolved.
Show resolved Hide resolved
@{{name}}_changed = true

@{{name}}_was = @{{name}}
end
{% if SETTERS[name] %}
Expand All @@ -302,127 +309,86 @@ abstract class ActiveModel::Model
# :nodoc:
# Adds the from_json method
macro __map_json__
{% if HAS_KEYS[0] && !PERSIST.empty? %}
JSON.mapping(
{% for name, opts in PERSIST %}
{% if opts[:converter] %}
{{name}}: { type: {{opts[:type_signature]}}, setter: false, converter: {{opts[:converter]}} },
{% else %}
{{name}}: { type: {{opts[:type_signature]}}, setter: false },
def after_initialize(trusted : Bool)
if !trusted
{% for name, opts in FIELDS %}
{% if !opts[:mass_assign] %}
@{{name}} = nil
{% end %}
{% end %}
)

# :nodoc:
def initialize(%pull : ::JSON::PullParser, trusted = false)
previous_def(%pull)
if !trusted
{% for name, opts in FIELDS %}
{% if !opts[:mass_assign] %}
@{{name}} = nil
{% end %}
{% end %}
end
apply_defaults
clear_changes_information
end

# Serialize from a trusted JSON source
def self.from_trusted_json(string_or_io : String | IO) : self
{{@type.name.id}}.new(::JSON::PullParser.new(string_or_io), true)
end
apply_defaults
clear_changes_information
end

YAML.mapping(
{% for name, opts in PERSIST %}
{% if opts[:converter] %}
{{name}}: { type: {{opts[:type_signature]}}, setter: false, converter: {{opts[:converter]}} },
{% else %}
{{name}}: { type: {{opts[:type_signature]}}, setter: false },
{% end %}
{% end %}
)

# :nodoc:
def initialize(%yaml : YAML::ParseContext, %node : ::YAML::Nodes::Node, _dummy : Nil, trusted = false)
previous_def(%yaml, %node, nil)
if !trusted
{% for name, opts in FIELDS %}
{% if !opts[:mass_assign] %}
@{{name}} = nil
{% end %}
{% end %}
end
apply_defaults
clear_changes_information
end
def self.from_json(string_or_io : String | IO, trusted : Bool = false) : self
super(string_or_io).tap &.after_initialize(trusted: trusted)
end

# Serialize from a trusted YAML source
def self.from_trusted_yaml(string_or_io : String | IO) : self
ctx = YAML::ParseContext.new
node = begin
document = YAML::Nodes.parse(string_or_io)

# If the document is empty we simulate an empty scalar with
# plain style, that parses to Nil
document.nodes.first? || begin
scalar = YAML::Nodes::Scalar.new("")
scalar.style = YAML::ScalarStyle::PLAIN
scalar
end
end
{{@type.name.id}}.new(ctx, node, nil, true)
end
# Serialize from a trusted JSON source
def self.from_trusted_json(string_or_io : String | IO) : self
self.from_json(string_or_io, trusted: true)
end

def assign_attributes_from_json(json)
json = json.read_string(json.read_remaining) if json.responds_to? :read_remaining && json.responds_to? :read_string
model = self.class.from_json(json)
data = JSON.parse(json).as_h
{% for name, opts in FIELDS %}
{% if opts[:mass_assign] %}
self.{{name}} = model.{{name}} if data.has_key?({{name.stringify}}) && self.{{name}} != model.{{name}}
{% end %}
{% end %}
def self.from_yaml(string_or_io : String | IO, trusted : Bool = false) : self
super(string_or_io).tap &.after_initialize(trusted: trusted)
end

self
end
# Serialize from a trusted YAML source
def self.from_trusted_yaml(string_or_io : String | IO) : self
self.from_yaml(string_or_io, trusted: true)
end

def assign_attributes_from_trusted_json(json)
json = json.read_string(json.read_remaining) if json.responds_to? :read_remaining && json.responds_to? :read_string
model = self.class.from_trusted_json(json)
data = JSON.parse(json).as_h
{% for name, opts in FIELDS %}
def assign_attributes_from_json(json)
json = json.read_string(json.read_remaining) if json.responds_to? :read_remaining && json.responds_to? :read_string
model = self.class.from_json(json)
data = JSON.parse(json).as_h
{% for name, opts in FIELDS %}
{% if opts[:mass_assign] %}
self.{{name}} = model.{{name}} if data.has_key?({{name.stringify}}) && self.{{name}} != model.{{name}}
{% end %}
{% end %}

self
end
self
end

# Uses the YAML parser as JSON is valid YAML
def assign_attributes_from_yaml(yaml)
yaml = yaml.read_string(yaml.read_remaining) if yaml.responds_to? :read_remaining && yaml.responds_to? :read_string
model = self.class.from_yaml(yaml)
data = YAML.parse(yaml).as_h
{% for name, opts in FIELDS %}
{% if opts[:mass_assign] %}
self.{{name}} = model.{{name}} if data.has_key?({{name.stringify}}) && self.{{name}} != model.{{name}}
{% end %}
{% end %}
# Assign each field from JSON if field exists in JSON and has changed in model
def assign_attributes_from_trusted_json(json)
json = json.read_string(json.read_remaining) if json.responds_to? :read_remaining && json.responds_to? :read_string
model = self.class.from_trusted_json(json)
data = JSON.parse(json).as_h
{% for name, opts in FIELDS %}
self.{{name}} = model.{{name}} if data.has_key?({{name.stringify}}) && self.{{name}} != model.{{name}}
{% end %}

self
end
self
end

def assign_attributes_from_trusted_yaml(yaml)
yaml = yaml.read_string(yaml.read_remaining) if yaml.responds_to? :read_remaining && yaml.responds_to? :read_string
model = self.class.from_trusted_yaml(yaml)
data = YAML.parse(yaml).as_h
{% for name, opts in FIELDS %}
# Uses the YAML parser as JSON is valid YAML
def assign_attributes_from_yaml(yaml)
yaml = yaml.read_string(yaml.read_remaining) if yaml.responds_to? :read_remaining && yaml.responds_to? :read_string
model = self.class.from_yaml(yaml)
data = YAML.parse(yaml).as_h
{% for name, opts in FIELDS %}
{% if opts[:mass_assign] %}
self.{{name}} = model.{{name}} if data.has_key?({{name.stringify}}) && self.{{name}} != model.{{name}}
{% end %}
{% end %}

self
end
self
end

{% end %}
def assign_attributes_from_trusted_yaml(yaml)
yaml = yaml.read_string(yaml.read_remaining) if yaml.responds_to? :read_remaining && yaml.responds_to? :read_string
model = self.class.from_trusted_yaml(yaml)
data = YAML.parse(yaml).as_h
{% for name, opts in FIELDS %}
self.{{name}} = model.{{name}} if data.has_key?({{name.stringify}}) && self.{{name}} != model.{{name}}
{% end %}

self
end
end

macro __nilability_validation__
Expand Down Expand Up @@ -496,22 +462,51 @@ abstract class ActiveModel::Model
{% end %}
end

# Declare attributes in real model
macro attribute(name, converter = nil, mass_assignment = true, persistence = true, **tags, &block)
# Declaring correct type of attribute
{% resolved_type = name.type.resolve %}
{% if resolved_type.nilable? %}
{% type_signature = resolved_type %}
{% else %}
{% type_signature = "#{resolved_type} | Nil".id %}
{% end %}

# Assign instance variable to correct type

dukenguyenxyz marked this conversation as resolved.
Show resolved Hide resolved
@[JSON::Field(
presence: true,
{% if !persistence %}
ignore: true,
{% end %}
{% if !converter.nil? %}
converter: {{converter}}
{% end %}
)]
@[YAML::Field(
presence: true,
{% if !persistence %}
ignore: true,
{% end %}
{% if !converter.nil? %}
converter: {{converter}}
{% end %}
)]
@{{name.var}} : {{type_signature.id}}

@[JSON::Field(ignore: true)]
@[YAML::Field(ignore: true)]
getter? {{name.var}}_present : Bool = false

# Attribute default value
def {{name.var.id}}_default : {{ name.type }}
dukenguyenxyz marked this conversation as resolved.
Show resolved Hide resolved
# Check if name.value is not nil
{% if name.value || name.value == false %}
{{ name.value }}
# Type is not nilable
{% elsif !resolved_type.nilable? %}
raise NilAssertionError.new("No default for {{@type}}{{'#'.id}}{{name.var.id}}" )
# Type is nilable
{% else %}
nil
{% end %}
Expand Down Expand Up @@ -544,7 +539,10 @@ abstract class ActiveModel::Model
type_signature: type_signature,
}
%}

{% HAS_KEYS[0] = true %}

# Declare default values if name.value is not nil
{% if name.value || name.value == false %}
{% DEFAULTS[name.var.id] = name.value %}
{% end %}
Expand Down